Написание сервисов Windows NT на WinAPI
Написание сервисов Windows NT на WinAPI
Написание сервисов 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; uses windows,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 - это показывает функции, что предыдущее поле было последним. У меня получилось так : begin dispatchtable[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 создаем поток var thid : 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; Источник: delphi.xonix.ru