| Написание сервисов Windows NT на WinAPI
 Причиной написания этой статьи, как не странно, стала необходимость написания своего сервиса. Но в Borland'е решили немного "порадовать" нас, пользователей Delphi 6 Personal, не добавив возможности создания сервисов (в остальных версиях Delphi 5 и 6 эта возможность имеется в виде класса TService). Решив, что еще не все потеряно, взял на проверку компоненты из одноименного раздела этого сайта. Первый оказался с многочисленными багами, а до пробы второго я не дошел, взглянув на исходник - модуль Forms в Uses это не только окошки, но и более 300 килобайт "веса" программы. Бессмысленного увеличения размера не хотелось и пришлось творить свое. Так как сервис из воздуха не сотворишь, то мой исходник и эта статья очень сильно опираются на MSDN. Итак, приступим к написанию своего сервиса...
 Обычный Win32-сервис это обычная программа. Программу рекомендуется сделать консольной (DELPHI MENU / Project / Options.. / Linker [X]Generate Console Application) и крайне рекомендуется сделать ее без форм !!! и удалить модуль Forms из Uses. Рекомендуется потому, что, во-первых, это окошко показывать не стоит потому, что оно позволит любому юзеру, прибив ваше окошко прибить и сервис и, во-вторых, конечно же, размер файла (19Kb против 350 ). Поэтому удаляем форму (DELPHI MENU / Project / Remove from project... ). Удалив все формы, перейдем на главный модуль проекта, в котором удаляем текст между begin и end и Forms из Uses и добавляем Windows и WinSvc. В результате должно получиться что-то вроде этого program Project1;usesWindows,WinSvc;
 {$R *.res} begin end. На этом подготовительный этап закончен - начинаем писать сервис. Главная часть программы Как уже отмечалось - сервис это обычная программа. Программа в Pascal'е находится между begin и end. После запуска нашего сервиса (здесь и далее под запуском сервиса понимается именно запуск его из Менеджера сервисов, а не просто запуск exe'шника сервиса) менеджер сервисов ждет пока наш сервис вызовет функцию StartServiceCtrlDispatcher.Ждать он будет недолго - если в нашем exe'шнике несколько сервисов то секунд 30, если один - около секунды, поэтому помещаем вызов StartServiceCtrlDispatcher поближе к begin. 
 StartServiceCtrlDispatcher качестве аргумента требует _SERVICE_TABLE_ENTRYA, поэтому добавляем в var DispatchTable : array [0..кол-во сервисов] of _SERVICE_TABLE_ENTRYA; и заполняем этот массив (естественно перед вызовом StartServiceCtrlDispatcher).
 Т.к. в нашем ехешнике будет 1 сервис, то заполняем его так :  DispatchTable[0].lpServiceName:=ServiceName;DispatchTable[0].lpServiceProc:=@ServiceProc;
  DispatchTable[1].lpServiceName:=nil;DispatchTable[1].lpServiceProc:=nil;
 Советую завести константы ServiceName - имя сервиса и ServiceDisplayName - отображаемое имя.ServiceProc - основная функция сервиса(о ней ниже), а в функцию мы передаем ее адрес.
 В DispatchTable[кол-во сервисов] все равно nil - это показывает функции, что предыдущее поле было последним. У меня получилось так :
 beginDispatchTable[0].lpServiceName:=ServiceName;
 DispatchTable[0].lpServiceProc:=@ServiceProc;
  DispatchTable[1].lpServiceName:=nil;DispatchTable[1].lpServiceProc:=nil;
  if not StartServiceCtrlDispatcher(DispatchTable[0])then LogError('StartServiceCtrlDispatcher Error');
 end.
 StartServiceCtrlDispatcher выполнится только после того, как все сервисы будут остановлены.
 Функция LogError протоколирует ошибки - напишите ее сами. Функция ServiceMain ServiceMain - основная функция сервиса. Если в ехешнике несколько сервисов, но для каждого сервиса пишется своя ServiceMain функция. Имя функции может быть любым! и передается в DispatchTable.lpServiceProc:=@ServiceMain (см.предыдущущий абзац). У меня она называется ServiceProc и описывается так:procedure ServiceProc(argc : DWORD;var argv : array of PChar);stdcall;
 argc кол-во аргументов и их массив argv передаются менеджером сервисов из настроек сервиса. НЕ ЗАБЫВАЙТЕ STDCALL!!! Такая забывчивость - частая причина ошибки в программе.
 В ServiceMain требуется выполнить подготовку к запуску сервиса и зарегистрировать обработчик сообщений от менеджера сервисов (Handler). Опять после запуска ServiceMain и до запуска RegisterServiceCtrlHandler должно пройти минимум времени. Если сервису надо делать что-нибудь очень долго и обязательно до вызова RegisterServiceCtrlHandler, то надо посылать сообщение SERVICE_START_PENDING функией SetServiceStatus.
 Итак, в RegisterServiceCtrlHandler передаем название нашего сервиса и адрес функции Handler'а (см.далее). Далее выполняем подготовку к запуску и настройку сервиса. Остановимся на настройке поподробнее.Эта самая настройка var ServiceStatus : SERVICE_STATUS;
 (ServiceStatusHandle : SERVICE_STATUS_HANDLE и ServiceStatus надо сделать глобальными переменными и поместить их выше всех функций).
 
| dwServiceType - тип сервиса |  
| SERVICE_WIN32_OWN_PROCESS | Одиночный сервис |  
| SERVICE_WIN32_SHARE_PROCESS | Несколько сервисов в одном процессе |  
| SERVICE_INTERACTIVE_PROCESS | интерактивный сервис (может взаимодействовать с пользователем). |  Остальные константы - о драйверах. Если надо - смотрите их в MSDN. 
 
| dwControlsAccepted - принимаемые сообщения (какие сообщения мы будем обрабатывать) |  
| SERVICE_ACCEPT_PAUSE_CONTINUE | приостановка/перезапуск |  
| SERVICE_ACCEPT_STOP | остановка сервиса |  
| SERVICE_ACCEPT_SHUTDOWN | перезагрузка компьютера |  
| SERVICE_ACCEPT_PARAMCHANGE | изменение параметров сервиса без перезапуска (Win2000 и выше) |  Остальные сообщения смотрите опять же в MSDN (куда уж без него ;-)
 dwWin32ExitCode и dwServiceSpecificExitCode - коды ошибок сервиса. Если все идет нормально, то они должны быть равны нулю, иначе коду ошибки.
 dwCheckPoint - если сервис выполняет какое-нибудь долгое действие при остановке, запуске и т.д. то dwCheckPoint является индикатором прогресса (увеличивайте его, чтобы дать понять, что сервис не завис), иначе он должен быть равен нулю.
 dwWaitHint - время, через которое сервис должен послать свой новый статус менеджеру сервисов при выполнении действия (запуска, остановки и т.д.). Если dwCurrentState и dwCheckPoint через это кол-во миллисекунд не изменится, то менеджер сервисов решит, что произошла ошибка.
 dwCurrentState - Ставим его в SERVICE_RUNNING, если сервис запущен После заполнения этой структуры посылаем наш новый статус функцией SetServiceStatus и мы работаем :).
 После этого пишем код самого сервиса. Я вернусь к этому попозже.Вот так выглядит моя ServiceMain :
 procedure ServiceProc(argc : DWORD;var argv : array of PChar);stdcall;var
 Status : DWORD;
 SpecificError : DWORD;
 begin
 ServiceStatus.dwServiceType := SERVICE_WIN32;
 ServiceStatus.dwCurrentState := SERVICE_START_PENDING;
 ServiceStatus.dwControlsAccepted := SERVICE_ACCEPT_STOP
 or SERVICE_ACCEPT_PAUSE_CONTINUE;
 ServiceStatus.dwWin32ExitCode := 0;
 ServiceStatus.dwServiceSpecificExitCode := 0;
 ServiceStatus.dwCheckPoint := 0;
 ServiceStatus.dwWaitHint := 0;
  ServiceStatusHandle := RegisterServiceCtrlHandler(ServiceName,@ServiceCtrlHandler);
 if ServiceStatusHandle = 0 then WriteLn('RegisterServiceCtrlHandler Error');
  Status :=ServiceInitialization(argc,argv,SpecificError);if Status <> NO_ERROR
 then begin
 ServiceStatus.dwCurrentState := SERVICE_STOPPED;
 ServiceStatus.dwCheckPoint := 0;
 ServiceStatus.dwWaitHint := 0;
 ServiceStatus.dwWin32ExitCode:=Status;
 ServiceStatus.dwServiceSpecificExitCode:=SpecificError;
  SetServiceStatus (ServiceStatusHandle, ServiceStatus);LogError('ServiceInitialization');
 exit;
 end;
  ServiceStatus.dwCurrentState :=SERVICE_RUNNING;ServiceStatus.dwCheckPoint :=0;
 ServiceStatus.dwWaitHint :=0;
  if not SetServiceStatus (ServiceStatusHandle,ServiceStatus)then begin
 Status:=GetLastError;
 LogError('SetServiceStatus');
 exit;
 end;
 // WORK HERE
 //ЗДЕСЬ БУДЕТ ОСНОВНОЙ КОД ПРОГРАММЫ
 end;
 Функция Handler Функция Handler будет вызываться менеджером сервисов при передаче сообщений сервису. Опять же название функции - любое. Адрес функции передается с помощью функции RegisterServiceCtrlHandler (см. выше). Функция имеет один параметр типа DWORD (Cardinal) - сообщение сервису. Если в одном процессе несколько сервисов - для каждого из них должна быть своя функция.procedure ServiceCtrlHandler(Opcode : Cardinal);stdcall;
 Опять не забываем про stdcall.
 Итак, функция получает код сообщения, который мы и проверяем. Начинаем вспоминать, что мы писали в ServiceStatus.dwControlsAccepted. У меня это SERVICE_ACCEPT_STOP и SERVICE_ACCEPT_PAUSE_CONTINUE, значит, мне надо проверять сообщения SERVICE_CONTROL_PAUSE, SERVICE_CONTROL_CONTINUE, SERVICE_CONTROL_STOP и выполнять соответствующие действия. Остальные сообщения: 
 
| ServiceStatus.dwControlsAccepted | Обрабатываемые сообщения |  
| SERVICE_ACCEPT_PAUSE_CONTINUE | SERVICE_CONTROL_PAUSE и SERVICE_CONTROL_CONTINUE |  
| SERVICE_ACCEPT_STOP | SERVICE_CONTROL_STOP |  
| SERVICE_ACCEPT_SHUTDOWN | SERVICE_CONTROL_SHUTDOWN |  
| SERVICE_ACCEPT_PARAMCHANGE | SERVICE_CONTROL_PARAMCHANGE |  
 Также надо обрабатывать SERVICE_CONTROL_INTERROGATE. Что это такое - непонятно, но обрабатывать надо :) Передаем новый статус сервиса менеджеру сервисов функцией SetServiceStatus.
 Пример функции Handler: procedure ServiceCtrlHandler(Opcode : Cardinal);stdcall;var
 Status : Cardinal;
 begin
 case Opcode of
 SERVICE_CONTROL_PAUSE :
 begin
 ServiceStatus.dwCurrentState := SERVICE_PAUSED;
 end;
 SERVICE_CONTROL_CONTINUE :
 begin
 ServiceStatus.dwCurrentState := SERVICE_RUNNING;
 end;
 SERVICE_CONTROL_STOP :
 begin
 ServiceStatus.dwWin32ExitCode:=0;
 ServiceStatus.dwCurrentState := SERVICE_STOPPED;
 ServiceStatus.dwCheckPoint :=0;
 ServiceStatus.dwWaitHint :=0;
  if not SetServiceStatus (ServiceStatusHandle,ServiceStatus)then begin
 Status:=GetLastError;
 LogError('SetServiceStatus');
 Exit;
 end;
 exit;
 end;
  SERVICE_CONTROL_INTERROGATE : ;end;
  if not SetServiceStatus (ServiceStatusHandle, ServiceStatus)then begin
 Status := GetLastError;
 LogError('SetServiceStatus');
 Exit;
 end;
 end;
 Реализация главной функции В функции ServiceMain (см.там, где отмечено) пишем код сервиса. Так как сервис обычно постоянно находится в памяти компьютера, то скорее всего код будет находиться в цикле. Например в таком : repeatЧто-нибудь делаем пока сервис не завершится.
 until ServiceStatus.dwCurrentState = SERVICE_STOPPED;
 
 Но это пройдет если сервис не обрабатывает сообщения приостановки/перезапуска, иначе сервис никак не прореагирует. Другой вариант : repeat if ServiceStatus.dwCurrentState <> SERVICE_PAUSED
 then чего-то делаем
 until ServiceStatus.dwCurrentState = SERVICE_STOPPED;
 
 И третий, имхо, самый правильный вариант = использование потока :Пишем функцию
 function MainServiceThread(p:Pointer):DWORD;stdcall;begin
 что-то делаем
 end;
 
 и в ServiceMain создаем поток varThID : Cardinal;
 hThread:=CreateThread(nil,0,@MainServiceThread,nil,0,ThID);и ждем его завершения
 WaitForSingleObject(hThread,INFINITE);
 закрывая после этого его дескриптор
 CloseHandle(hThread);
 При этом hThread делаем глобальной переменной.Теперь при приостановке сервиса (в Handler) делаем так
  SERVICE_CONTROL_PAUSE :begin
 ServiceStatus.dwCurrentState := SERVICE_PAUSED;
 SuspendThread(hThread); // приостанавливаем поток
 end;
 и при возобновлении работы сервиса  SERVICE_CONTROL_CONTINUE :begin
 ServiceStatus.dwCurrentState := SERVICE_RUNNING;
 ResumeThread(hThread); // возобновляем поток
 end;
 
 |