Створення служб Windows в Delphi з використанням VCL, Різне, Програмування, статті

Автор: © Олександр Алексєєв


Стаття присвячена питанням створення служб (сервісів) Windows в Delphi з використанням VCL, тобто не на Windows API (WinAPI). Вона призначена для людей, які збираються написати або вже написали свою першу службу Windows. Стаття не претендує на повноту або унікальність. Деякі речі залишилися за бортом, особливо це стосується функцій WinAPI. Але їх можна подивитися самостійно в довідці по Delphi, вихідним кодами VCL і MSDN / Platform SDK. Особливо глибоких знань не потрібно – для розуміння статті читач повинен бути знайомий зі службами Windows на рівні користувача. В деяких місцях виклад може забігати вперед. Тому якщо вам зустрівся незрозумілий момент – просто пропустіть його, можливо, потім він проясниться.


Вступ


Служба або сервіс Windows – це звичайна програма Windows, але написане за певними правилами. Такі програми містять додатковий код, що дозволяє стандартному менеджеру служб Windows (SCM – Service Control Manager) одноманітно керувати процесами служб. SCM розташовується у файлі services.exe, запускається при старті системи і зупиняється тільки при завершенні роботи ОС. До речі кажучи, в одному exe-файлі може розміщуватися кілька служб. Зазвичай це робиться для економії ресурсів та спрощення взаємодії між службами, тому що служби будуть знаходитися в одному адресному просторі. Зокрема, майже всі стандартні служби Windows саме так і реалізовані: більшість з них знаходяться в DLL, які завантажуються в svchost.exe.


Всі стандартні серверні додатки Windows реалізовані у вигляді служб. SCM дозволяє адміністраторам керувати службами локальної або навіть віддаленої машини. Служби можна запускати, зупиняти, припиняти і відновлювати. Крім цього, SCM стежить за виконанням служб і здатний виконувати додаткові дії при раптовій зупинці небудь служби (раптова зупинка – це коли служба зупинилася, але не повідомила про це).


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


Саме тому для однорідного управління службами служить SCM, а для їх налаштування – якесь зовнішнє додаток, що може “спілкуватися” зі службою або змінювати конфігурацію служби (наприклад, записуючи її до реєстру). У разі стандартних служб нерідко таким конфігураційним додатком виступає оснастка до MMC (Microsoft Management Console). Стандартизація засобів управління і конфігурування спрощує життя і розробникам служби та системним адміністраторам.


Як і у випадку стандартних додатків Windows, Delphi дозволяє легко і швидко написати службу, використовуючи VCL (на жаль, чого не скажеш про оснащенні до MMC).


Установка і видалення служби


Нам бажано працювати під обліковим записом адміністратора, тому що за замовчуванням, звичайний користувач не має права на багато операцій зі службами, наприклад – на їх установку.


Отже, запускаємо Delphi, відкриваємо в головне меню пункт File / New / Other, вибираємо “Service Application”:














Малюнок 1а

Малюнок 1б

Після цього Delphi створить пусте додаток служби. Зауважимо, що в новому проекті були автоматично згенеровані дві підпрограми:










procedure ServiceController(CtrlCode: DWord); stdcall;
begin
Service1.Controller(CtrlCode);
end;
function TService1.GetServiceController: TServiceController;
begin
Result := ServiceController;
end;


Ці два методи ми ніколи чіпати не будемо. Крім коду, перед собою ми бачимо порожнє вікно: це вікно TService – спадкоємця від TDataModule, що представляє нашу службу.


Як вже було сказано, в одному проекті (відповідно, exe-файлі) може бути кілька служб. Щоб додати ще одну службу в поточний проект, ліземо в File / New / Other і там вибираємо “Service” (а не “Service Application “, як було раніше). В BDS пункт” Service “в розділі” Delphi Files “з’являється тільки, якщо поточний проект є проектом служби. В Delphi 7 і нижче пункт” Service “видно завжди, він завжди створює порожній модуль з TService.


У цей самий вікно служби ми можемо додавати невізуальні компоненти, якщо вони нам знадобляться. Візуальні компоненти ми використовувати не можемо зі зрозумілих причин: в кінці кінців, ми пишемо фонову службу, яка може працювати, навіть якщо користувач не зайшов в систему, тому ми не можемо використовувати жодних інтерактивних засобів взаємодії з користувачем.


Як завжди, навіть мінімальне VCL додаток володіє необхідним мінімумом для функціонування. Порожній проект ми можемо скомпілювати і навіть встановити. Для цього копіюємо скомпільований exe-файл в C: WindowsSystem32 (Або в еквівалентну папку на вашій машині). Копіювання – опціонально, але рекомендується; взагалі кажучи, exe-файл служби може знаходитися в будь-якому місці (з урахуванням деяких особливостей: наприклад, файл служби не може розташовуватися на мережевому або subst-диску, тому що вони створюються тільки при вході користувача в систему). Потім запускаємо exe-файл з параметром “/ install”, наприклад:










Project1.exe /install



Рисунок 2

Служба повідомить, що вона успішно встановлена:



Рисунок 6

Як бачимо, кожен з’єднання в реєстрі відповідає розділу логів (ліворуч) в “Перегляд подій”:










  EventLog
Application
AppName
Security
System
DriverName
CustomLog
AppName


Ви можете додавати свої події в один з існуючих розділів (зазвичай, в Application) або створити свій розділ (на діаграмі вище – позначений CustomLog, не підтримується в Windows NT).











Бонус-пак: створення свого журналу.


Якщо ви захочете додати свій пункт (в оснащенні “Перегляд подій” він з’явиться ліворуч, разом з Додаток / Безпека / Система), то ви додаєте підрозділ в ключі EventLog:

  EventLog
CustomLog

У цьому підрозділі створюєте параметри (всі вони – необов’язкові):


DisplayNameFile (REG_EXPAND_SZ) і DisplayNameID (REG_DWORD) – вказують локалізовану (тобто, “по-російськи”) рядок, який буде видно в “Перегляд подій”. Як задати цей рядок і чому параметрів два, ми обговоримо нижче.


MaxSize (REG_DWORD) – максимальний розмір лог-файла в байтах, у стандартних логів дорівнює 0x80000 (524288).


RestrictGuestAccess (REG_DWORD) – 0 або 1, заборонити доступ для гостей.


Retension (REG_DWORD) – час максимальної життя записів в секундах. По-замовчуванням дорівнює 7 дням.


Незалежно від того, додаєте ви свій розділ логів або використовуєте розділ Application, наступним кроком ви створюєте підрозділ з ім’ям своєї служби (те, що записано в TService.Name), наприклад:










  EventLog
Application
Service1


Далі, в свіжоствореному ключі додаєте наступні параметри (а от вони вже обов’язкові):


EventMessageFile (REG_EXPAND_SZ) – шлях до exe, який містить вашу службу, наприклад “% SystemRoot% System32Project1.exe” (без лапок, звісно). Можна вказати через крапку з комою кілька файлів (exe або DLL), якщо ви робите таблиці повідомлень в різних файлах.


TypesSupported (REG_DWORD) – типи повідомлень, які ви підтримуєте. Зазвичай дорівнює 7 (в десятковій формі) – комбінація прапорів EVENTLOG_XXX (див. вище в пункті 4), що означає події типу помилка + повідомлення + Попередження.


Для створення набору рядків для логів нам треба створити текстовий файл з розширенням. Mc, вписати туди набір рядків у певному форматі і обробити його компілятором MC (MessageCompiler), після чого результат компіляції зібрати в ресурсний файл компілятором ресурсів RC (Resource Compiler), потім готовий. res-файл просто підключається до проекту. Компілятори MC і RC можна взяти з Platform SDK або MSDN. Параметри запуску кожного компілятора можна подивитися, запустивши їх з командного рядка з параметром /?.


Для компіляції в один файл, що підключається до exe-файлу служби, зручно скласти приблизно такий bat-файл:










@echo off
del msg.res > nul
binmc -u -U msg.mc
del msg.h > nul
bin c -r msg.rc
del msg.rc > nul
del MSG*.bin > nul
echo Done.
pause


Тут передбачається, що ми компілюємо msg.mc (у форматі Unicode, якщо ж компілюється ANSI-файл, то третій рядок буде виглядати так: “binmc-a-U msg.mc”), з нього виходить msg.rc, msg.h і пачка файлів MSGxxxx.bin (по одному на кожну мову), потім з усього цього збирається файл msg.res, а проміжні файли видаляються. Компілятори повинні лежати в папці bin поточної папки. Якщо це не так – в bat-ніку потрібно підправити шляху (“binmc” і “binc “). Для підключення готового res-файла додаємо в проект сходинку {$ R msg.res}, наприклад, відразу після {$ R *. dfm}.


. Mc-файл має приблизно такий зміст:










MessageIdTypedef=DWORD
LanguageNames=(English=0x409:MSG00409)
LanguageNames=(Russian=0x419:MSG00419)
MessageId=0x1
Language=English
%1
.
Language=Russian
%1
.
MessageId=0x2
Severity=Error
Language=English
Error: %1
.
Language=Russian Помилка:% 1
.
MessageId=0x3
Severity=Error
Language=English
Daemon was not configured
.
Language=Russian Демон не був налаштований
.
MessageId=0x4
Severity=Error
Language=English
Unhandled exception raised: %1
.
Language=Russian Виникло необроблене виняток:% 1
.
MessageId=0x5
Severity=Error
Language=English
Unhandled exception raised at create: %1
.
Language=Russian Виникло необроблене виняток при ініціалізації:% 1
.
MessageId=0x6
Severity=Error
Language=English
Unhandled exception raised at destroy: %1
.
Language=Russian Виникло необроблене виняток при завершенні:% 1
.
MessageId=0x7
Severity=Error
Language=English
Unhandled exception raised at start: %1
.
Language=Russian Виникло необроблене виняток при запуску:% 1
.
MessageId=0x8
Severity=Error
Language=English
Unhandled exception raised: %1
.
Language=Russian Виникло необроблене виняток при зупинці:% 1
.


Спочатку визначаються мови, на яких будуть писатися рядки в файлі (два рядки з LanguageNames на початку файлу). Ідентифікатори мов можна подивитися в MSDN / Platform SDK в розділі “Table of Language Identifiers “. Потім можуть визначатися SeverityNames і FacilityNames – типи повідомлень та типи” авторів “повідомлень, але за замовчуванням вони мають цілком пристойний вигляд:










SeverityNames=(
Success=0x0
Informational=0x1
Warning=0x2
Error=0x3
)
FacilityNames=(
System=0x0FF
Application=0xFFF
)


Тому, зазвичай ці рядки опускаються. Далі йдуть власне повідомлення. Повідомлення починається з MessageId = номер. У цих рядках повідомленнями присвоюються їх номери (номери вибираємо ми). Саме за цими номерами ми потім будемо додавати їх в лог. До речі, і цей параметр опціональний. Якщо його не вказувати, то перше повідомлення отримає номер 0, а наступні будуть збільшуватися на 1. Далі можуть йти набори з Severity і Facility, які за умовчанням мають вигляд:










Severity=Success
Facility=Application


Далі слід мову повідомлення і саме повідомлення. Точка на початку рядка означає кінець повідомлення. Якщо мов декілька, вони слідують один за одним. У самих повідомленнях можна використовувати спеціальні символи:

%n[!format_specifier!]

Описує вставку параметра. Кожен параметр занумерован від 1 до 99, format_specifier – необов’язковий параметр форматування (за замовчуванням -! S!). Список параметрів форматування такий же, як і у функції wsprintf мови C. Наприклад, в прикладі. Mc-файла вище це були параметри виду “% 1”. Надалі, зі служби замість цих параметрів можна буде вставляти довільні дані.

%0

Те ж, що і точка на початку рядка, але на відміну від точки, в кінці повідомлення не буде вставлений перенос рядка.

%.

Вставляє точку. Може використовуватися для вставки точки в початок рядка, без обриву повідомлення.

%%

Вставляє знак “%”.


Детальніше про вміст mc-файлів можна прочитати в MSDN / Platform SDK в розділі “Message Text Files”. Ось ще приклад mc-файла звідти:










; /* Sample.mc
;
; This is a sample message file. It contains a comment block, followed by a
; header section, followed by messages in two languages.
;
; */
; // This is the header section.
MessageIdTypedef=DWORD
SeverityNames=(Success=0x0:STATUS_SEVERITY_SUCCESS
Informational=0x1:STATUS_SEVERITY_INFORMATIONAL
Warning=0x2:STATUS_SEVERITY_WARNING
Error=0x3:STATUS_SEVERITY_ERROR
)
FacilityNames=(System=0x0:FACILITY_SYSTEM
Runtime=0x2:FACILITY_RUNTIME
Stubs=0x3:FACILITY_STUBS
Io=0x4:FACILITY_IO_ERROR_CODE
)
LanguageNames=(English=0x409:MSG00409)
LanguageNames=(Japanese=0x411:MSG00411)
; // The following are message definitions.
MessageId=0x1
Severity=Error
Facility=Runtime
SymbolicName=MSG_BAD_COMMAND
Language=English
You have chosen an incorrect command.
.
Language=Japanese / / Тут ієрогліфи 🙂
.
MessageId=0x2
Severity=Warning
Facility=Io
SymbolicName=MSG_BAD_PARM1
Language=English
Cannot reconnect to the server.
.
Language=Japanese / / Тут ієрогліфи 🙂
.
MessageId=0x3
Severity=Success
Facility=System
SymbolicName=MSG_STRIKE_ANY_KEY
Language=English
Press any key to continue . . . %0
.
Language=Japanese / / Тут ієрогліфи 🙂
.
MessageId=0x4
Severity=Error
Facility=System
SymbolicName=MSG_CMD_DELETE
Language=English
File %1 contains %2 which is in error
.
Language=Japanese / / Тут ієрогліфи 🙂
.
MessageId=0x5
Severity=Informational
Facility=System
SymbolicName=MSG_RETRYS
Language=English
There have been %1!d! attempts with %2!d!%% success%! Disconnect from the server and try again later.
.
Language=Japanese / / Тут ієрогліфи 🙂
.


Гаразд, в кінці-кінців ми отримали. Res-файл і підключили його до проекту. Тепер в нашому exe файлі (або в DLL, якщо ви підключали res-файл до проекту DLL) є таблиця рядків, на яку ми можемо посилатися. Якщо ви вирішили створити свій розділ для логів, то ви повинні пам’ятати, що ми згадували вище ключі реєстру DisplayNameFile (REG_EXPAND_SZ) і DisplayNameID (REG_DWORD). Тепер в DisplayNameFile можна вписати ім’я нашого exe / dll-файлу (з шляхом), а в DisplayNameID – номер повідомлення, яке містить назву нашого розділу логів.


Наприклад, якщо у файлі msg.mc були рядки:










MessageId=0x9
Language=English
My custom logs
.
Language=Russian Мій профіль логів
.


То в DisplayNameFile можна вписати, наприклад “% SystemRoot% System32Project1.exe”, а в DisplayNameID – 9.


Ну і тепер же ми можемо розібратися із методом TService.LogMessage:


LogMessage (Message: String; EventType: DWord; Category, ID: Integer)


В Message ми передаємо параметр (може бути “”, якщо в повідомленні немає “% 1”), тип події EventType – EVENTLOG_SUCCESS, EVENTLOG_ERROR_TYPE, EVENTLOG_WARNING_TYPE, EVENTLOG_INFORMATION_TYPE. Категорія – це будь-яке число, так само 0, якщо ви не використовуєте категорії. Категорії корисні для угруповання в групи великої кількості різнотипних повідомлень. Для простих служб, які ви будете писати на початку, це явно зайве, тому розгляд з категоріями можна відкласти на потім. ID – це номер повідомлення з MC-файла. Звернемо увагу, що в наведених вище прикладах mc-файлів номера повідомлень записувалися в шістнадцятковій системі числення.


Наприклад, щоб додати в лог повідомлення типу “служба не налаштована” (для самого першого прикладу mc-файлу), пишемо:


LogMessage (“”, EVENTLOG_WARNING_TYPE, 0, 3);


А щоб додати довільне повідомлення:


LogMessage (“довільний текст”, EVENTLOG_INFORMATION_TYPE, 0, 1);


На жаль, LogMessage має обмеження: ви можете використовувати лише 1 параметр.


Для того щоб використовувати кілька параметрів, вам доведеться написати свій аналог LogMessage, наприклад такий:











fEventLog: THandle;

/ / Десь в OnCreate для TService:
fEventLog := RegisterEventSource(nil, PChar(Name));

/ / Десь в OnDestroy для TService:
if fEventLog <> 0 then
begin
DeregisterEventSource(fEventLog);
fEventLog := 0;
end;

/ / Нова процедура LogMessage:
procedure TService.LogMessage(aID: Integer; aType: Integer; aMsg: String = “”; aMsgCount: Integer = 0);
var
P: array of PChar;
D: PChar;
X, Y: Integer;
begin
if fEventLog <> 0 then
if aMsgCount <= 0 then
ReportEvent(fEventLog, aType, 0, aID, nil, 0, 0, nil, nil)
else
if aMsgCount = 1 then
begin
D := PChar(aMsg);
ReportEvent(fEventLog, aType, 0, aID, nil, 1, 0, @D, nil);
end
else
begin
SetLength(P, aMsgCount);
for X := 0 to aMsgCount – 1 do
begin
Y := Pos(#0, aMsg);
if Y > 0 then
begin
P[X] := PChar(Copy(aMsg, 1, Y – 1));
aMsg := Copy(aMsg, Y + 1, MaxInt);
end
else
begin
P[X] := PChar(aMsg);
aMsg := “”;
end;
end;
ReportEvent(fEventLog, aType, 0, aID, nil, aMsgCount, 0, @(P [0]), nil);
end;
end;


Приклад виклику такої процедури:










LogMessage (11, EVENTLOG_INFORMATION_TYPE, “параметр 1” # 0 “параметр 2” # 0 “параметр 3”, 3);


До речі кажучи, повідомлення з підготовленої таблиці рядків можна ще витягти для власного використання через функцію FormatMessage.


Отже, підсумовуючи сказане, для додавання підтримки системних логів потрібно зробити наступне: пишемо набір повідомлень в. Mc-файл, для компіляції запускаємо чарівний bat-ник, а в самій програмі викликаємо LogMessage.


Додатково


На жаль, при своїй самоустановки служба не додає коментар до себе (ця можливість з’явилася в Windows 2000), а також не реєструє в реєстрі джерело логів. Тому, часто в службу доводиться Додавати обробники подій OnAfterInstall і OnAfterUninstall, наприклад, так:










/ / Після установки
procedure TService.ServiceAfterInstall(Sender: TService);
var
Reg: TRegIniFile;
begin
Reg := TRegIniFile.Create(KEY_ALL_ACCESS);
try
Reg.RootKey := HKEY_LOCAL_MACHINE; / / Створюємо системний лог для себе
Reg.OpenKey(“SYSTEMCurrentControlSetServicesEventlogApplication” + Name, True);
Reg.WriteString(“SYSTEMCurrentControlSetServicesEventlogApplication” + Name, “EventMessageFile”, ParamStr(0));
TRegistry(Reg).WriteInteger(“TypesSupported”, 7); / / Прописуємо собі опис Reg.WriteString (“SYSTEMCurrentControlSetServices” + Name, “Description”, “Сюди вписується опис вашої служби.”);
finally
FreeAndNil(Reg);
end;
end;
/ / Після видалення
procedure TService.ServiceAfterUninstall(Sender: TService);
var
Reg: TRegIniFile;
begin
Reg := TRegIniFile.Create(KEY_ALL_ACCESS);
try
Reg.RootKey := HKEY_LOCAL_MACHINE; / / Видалимо свій системний лог
Reg.EraseSection(“SYSTEMCurrentControlSetServicesEventlogApplication” + Name);
finally
FreeAndNil(Reg);
end;
end;


З приводу налагодження служби. По-перше, замість улюбленого багатьма програмістами ShowMessage в службі зручно використовувати процедуру OutputDebugString (оголошена в модулі Windows). Для перегляду налагоджувальних повідомлень можна використовувати відладчик Delphi (View / Event log) або (що більш зручно) – DebugView for Windows by Mark Russinovich


По-друге, можна використовувати стандартний Run / Attach to process для підключення відладчика Delphi до вже запущеної службі. На жаль, це не дасть налагоджувати код ініціалізації, т.к. до моменту підключення отладчика служба вже повинна бути запущена. Втім, обхідним шляхом можна вирішити проблему, наприклад, так: вставити в початок dpr файлу щось подібне Sleep (5000) і за цей час відразу після старту потрібно встигнути підключити відладчик Delphi.


До речі, для управління службами в Windows є консольні утиліти net.exe і sc.exe. Ви можете запустити їх з командного рядка без параметрів для перегляду списку виконуваних дій.


Final words


І пару слів на завершення теми.


З приводу обробки помилок в службах буде цікаво подивитися “Питання КС № 60359”


Ще можуть бути цікаві запитання з приводу програмного управління службою (аналог своєї оснастки “Служби”) – наприклад, “Питання КС № 59588”


Про небезпеки використання різних компонент без розуміння принципів їх роботи: “Питання КС № 55674” 🙂


Дещо по інтерактивним службам: “Питання КС № 58832”, “Питання КС № 59632”.


Крім того, на Королівстві є кілька непоганих статей з питання використання стандартних засобів захисту Windows


Зокрема, у статті Приклад використання Private Object Security в Delphi можна подивитися приклад готової служби.


У цій статті не розглядалися питання написання програм для MMC. В Delphi немає підтримки написання таких додатків. Але в Інтернеті можна знайти компоненти, що спрощують їх розробку, наприклад: MMC Snapin Framework.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*