На главную Наши проекты:
Журнал   ·   Discuz!ML   ·   Wiki   ·   DRKB   ·   Помощь проекту
ПРАВИЛА FAQ Помощь Участники Календарь Избранное RSS
msm.ru
Модераторы: jack128, Rouse_, Krid
  
    > [Раздел] Сервисы

      Написание сервисов Windows NT на WinAPI

      Источник: delphi.xonix.ru

      Причиной написания этой статьи, как не странно, стала необходимость написания своего сервиса. Но в 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;


      <!-- Part 5 -->

      Функция 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;


       

        Как запустить и остановить сервис (или получить его статус)

        Здесь представлены две функции ServiceStart и ServiceStop, которые показывают, как пользоваться API функциями OpenSCManager, OpenService и т.д.:

        ExpandedWrap disabled
          function ServiceStart(aMachine, aServiceName : string ) : boolean;
          // aMachine это UNC путь, либо локальный компьютер если пусто
          var
            h_manager,h_svc: SC_Handle;
            svc_status: TServiceStatus;
            Temp: PChar;
            dwCheckPoint: DWord;
          begin
            svc_status.dwCurrentState := 1;
            h_manager := OpenSCManager(PChar(aMachine), Nil,
                                       SC_MANAGER_CONNECT);
            if h_manager > 0 then
            begin
              h_svc := OpenService(h_manager, PChar(aServiceName),
                                   SERVICE_START or SERVICE_QUERY_STATUS);
              if h_svc > 0 then
              begin
                temp := nil;
                if (StartService(h_svc,0,temp)) then
                  if (QueryServiceStatus(h_svc,svc_status)) then
                  begin
                    while (SERVICE_RUNNING <> svc_status.dwCurrentState) do
                    begin
                      dwCheckPoint := svc_status.dwCheckPoint;
           
                      Sleep(svc_status.dwWaitHint);
           
                      if (not QueryServiceStatus(h_svc,svc_status)) then
                        break;
           
                      if (svc_status.dwCheckPoint < dwCheckPoint) then
                      begin
                        // QueryServiceStatus не увеличивает dwCheckPoint
                        break;
                      end;
                    end;
                  end;
                CloseServiceHandle(h_svc);
              end;
              CloseServiceHandle(h_manager);
            end;
            Result := SERVICE_RUNNING = svc_status.dwCurrentState;
          end;
           
          function ServiceStop(aMachine,aServiceName : string ) : boolean;
          // aMachine это UNC путь, либо локальный компьютер если пусто
          var
            h_manager,h_svc   : SC_Handle;
            svc_status     : TServiceStatus;
            dwCheckPoint : DWord;
          begin
            h_manager:=OpenSCManager(PChar(aMachine),nil,
                                     SC_MANAGER_CONNECT);
            if h_manager > 0 then
            begin
              h_svc := OpenService(h_manager,PChar(aServiceName),
                                   SERVICE_STOP or SERVICE_QUERY_STATUS);
           
              if h_svc > 0 then
              begin
                if(ControlService(h_svc,SERVICE_CONTROL_STOP,
                                  svc_status))then
                begin
                  if(QueryServiceStatus(h_svc,svc_status))then
                  begin
                    while(SERVICE_STOPPED <> svc_status.dwCurrentState)do
                    begin
                      dwCheckPoint := svc_status.dwCheckPoint;
                      Sleep(svc_status.dwWaitHint);
           
                      if(not QueryServiceStatus(h_svc,svc_status))then
                      begin
                        // couldn't check status
                        break;
                      end;
           
                      if(svc_status.dwCheckPoint < dwCheckPoint)then
                        break;
           
                    end;
                  end;
                end;
                CloseServiceHandle(h_svc);
              end;
              CloseServiceHandle(h_manager);
            end;
           
            Result := SERVICE_STOPPED = svc_status.dwCurrentState;
          end;


        Чтобы узнать состояние сервиса, используйте следующую функцию:

        ExpandedWrap disabled
          function ServiceGetStatus(sMachine, sService: string ): DWord;
          var
            h_manager,h_service: SC_Handle;
            service_status     : TServiceStatus;
            hStat : DWord;
          begin
            hStat := 1;
            h_manager := OpenSCManager(PChar(sMachine) ,Nil,
                                       SC_MANAGER_CONNECT);
           
            if h_manager > 0 then
            begin
              h_svc := OpenService(h_manager,PChar(sService),
                                SERVICE_QUERY_STATUS);
           
              if h_svc > 0 then
              begin
                if(QueryServiceStatus(h_svc, service_status)) then
                  hStat := service_status.dwCurrentState;
           
                CloseServiceHandle(h_svc);
              end;
              CloseServiceHandle(h_manager);
            end;
            Result := hStat;
          end;


        Она возвращает одну из следующих констант:

        SERVICE_STOPPED
        SERVICE_RUNNING
        SERVICE_PAUSED
        SERVICE_START_PENDING
        SERVICE_STOP_PENDING
        SERVICE_CONTINUE_PENDING
        или
        SERVICE_PAUSE_PENDING

        Всё что, что Вам нужно, это unit WinSvc !
        0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)
        0 пользователей:


        Рейтинг@Mail.ru
        [ Script execution time: 0,0199 ]   [ 16 queries used ]   [ Generated: 16.04.24, 09:55 GMT ]