Delphi és Win32 API – Nem csak Delphi felhasználóknak: VII. rész

Windows NT Szolgáltatások – 1.

A Borland Delphi rendszer számos, a Windowsban (és a Linuxban) használt rendszereszközhöz tartalmaz komponenseket, osztályokat. Mégis érhetetlen módon több elem hiányzik belőle, melyek sokszor vagy csak ritkán, de jól jönnének egy program fejlesztése során. Cikksorozatunkban ilyen komponenseket fogunk készíteni. A sorozatot más programnyelvek használóinak is ajánlom, hiszen a bemutatott Windows API függvények bizonyára más nyelveken is elérhetőek.

Manapság számos olyan program kerül a piacra, melyek különböző szolgáltatásokkal bővítik Windowsunkat. Általában valamilyen szerver programról van ilyenkor szó (pl. egy webszerver, egy mailszerver, proxy szerver, stb.). Miben is különböznek ezek a programok a „hagyományos” programoktól?

Először is tisztázni szeretnék egy nagyon fontos tévhitet: sokakban az a téveszme él, hogy ha valamihez odaírják, hogy „szerver”, akkor az már csak valamilyen Windows Server-en képes működni. Annak ellenére, hogy valóban vannak ilyen programok, sokuk mégis elfut akár egy egyszerű asztali Windows-on is, igaz ott csak korlátozott képességekkel.

A Windows NT és az őt követő rendszerekben (Windows 2000, Windows XP, stb.) megtalálható egy speciális programfuttatási mód. Ez a szolgáltatás. Egy szolgáltatás lényegében egy olyan speciális program, amely akkor is futhat, amikor éppen nincsen egyetlen felhasználó se bejelentkezve az adott gépen. Ezek a programok általában már a Windows indulásakor elindulnak, még mielőtt egy felhasználó bejelentkezve. Ezzel lehet garantálni például, hogy egy tűzfal program már akkor is védelmet nyújtson a számítógép számára, amikor még nem jelentkezett be egyetlen felhasználó sem. Ilyen szolgáltatásokként futnak a rendszert működtető meghajtó programok is.

Hogy éppen milyen szolgáltatások érhetőek el a gépünkön, mi is megtekinthetjük: Windows 2000 és újabb operációs rendszereknél, ha a jobb egérgombbal klikkelünk a Sajátgép (My Computer) ikonra és kiválasztjuk a Kezelés menüpontot, megjelenik egy ablak. A fanézet alsó soraiban találhatunk egy Szolgáltatások ikont, melyre ráklikkelve megjelennek az éppen elérhető szolgáltatások.

A most következő három részben ezen szolgáltatások kezeléséről (és nem készítéséről) lesz szó.

 

Alapfogalmak, áttekintés

 

Először is tisztázzunk néhány fogalmat a témakörrel kapcsolatban. Itt most viszonylag hosszan egy olyan bekezdés következik, mely amolyan gyorstalpaló stílusban mutatja be a Windows Szolgáltatások egyes tulajdonságait. Aki ezeket már ismeri, nyugodtan átugorhatja.

A Windows Szolgáltatásokat az ún. Service Control Manager (SCM) kezeli, rajta keresztül kérhetünk le információkat, irányíthatjuk az elérhető szolgáltatásokat. Miután kapcsolódtunk hozzá a megfelelő API függvénnyel, lekérhetjük a telepített szolgáltatások listáját, és egyenként irányíthatjuk azokat.

Alapvetően kétféle csoportját különböztetjük meg a Windows NT szolgáltatásoknak: egyrészt vannak a meghajtó programok (drivers), ezekhez, ha nem szükséges ne nyúlkáljunk, tapasztalt favágó nem vágja maga alatt a fát ;). A másik csoportot úgy nevezném, hogy minden egyéb.

Egy szolgáltatás különböző állapotokban lehet: Leállított (Stopped) állapotban nem fut, az általa megvalósított funkciók nem érhetőek el ilyenkor. Amikor elindítjuk, vagy a rendszer elindítja, egy amolyan bemelegedési folyamat indul el, egy inicializálás, mely, ha nagyon hosszú, akkor jönnek a Windows Gyűlölők Egyletének tagjai és panaszkodnak, milyen lassan indul a rendszer :). Ezt az inicializációs állapotot is külön jelzi nekünk az SCM (Start Pending állapot). Ha minden sikeres volt, a szolgáltatás elindult és Elindítva (Running) állapotba kerül. Nem minden szolgáltatás támogatja a szüneteltetés funkciót. Ilyenkor amolyan tetszhalottat játszva nem reagál a kéréseinkre, de nem is szabadítja fel az általa lefoglalt erőforrásokat (vagy azoknak csak egy részét). A szüneteltetés állapotba jutás előtt is van egy „átállás” állapot (Pause Pending), amit a Felfüggesztve (Paused) állapot követ. Ebből az állapotból a folytatás (Countiue Pending) paranccsal lehet továbbfuttatni a szolgáltatást, mely után ismét Elindítva (Running) állapotba kerül. Innen lehet leállítani. Ilyenkor felszabadítja (normális működés esetén) az általa lefoglalt memóriát, erőforrásokat (Stop Pending) és leáll (Stopped).

Mint már említettem, egy ilyen program futtatásához nem kell, hogy egy felhasználó be legyen jelentkezve. Csak hogy valahogy kontrolálni kell az operációs rendszernek, hogy miket tehet meg és miket nem. Ennek megoldására minden szolgáltatáshoz hozzárendelhetjük, hogy melyik felhasználóként fusson. Így készíthetünk akár egy speciális felhasználót, aki a hagyományos módon nem jelentkezik be, viszont tiltunk vagy engedünk bizonyos hozzáféréseket a merevlemezen. Ha a szolgáltatás ezzel a felhasználóval indul el, akkor érvényesek lesznek rá  - és csak rá – ezek a korlátozások. Létezik egy speciális felhasználó, az ún. LocalSystem. A LocalSystem felhasználónak teljes hozzáférése van a helyi számítógéphez, és ha az egy tartomány része, akkor valamennyi erőforráshoz, melyet a számítógép el tud érni a hálózaton (megosztott mappák, nyomtatók, stb.). Ebből kifolyólag nagyon oda kell figyelni, hogy mely szolgáltatásokat engedünk LocalSystem felhasználóként futni.

Egy Windows szolgáltatás indítási mód szerint négyféle állapotban lehet: Rendszer bootolásnál (Boot Time) a program az operációs rendszer bootolása során indul el. Ilyen indítási módja csak a meghajtó programoknak lehet. Rendszer indulás esetén a szolgáltatás a boot folyamat után, az első felhasználó bejelentkezése előtt indul el. Amennyiben az előző kettő közül egyikkel se élünk, Kézi (On Demand) indításra állíthatjuk. Ilyenkor a felhasználó kérésére indul el. Bizonyos esetekben előfordulhat, hogy meg szeretnénk akadályozni, hogy egyáltalán el lehessen indítani egy szolgáltatást. Ilyenkor Tiltott (Disabled) állapotba lehet állítani.

És végül még egy dolog. Amennyiben egy szolgáltatás indítása/futása során problémák merülnek fel, megmondhatjuk az indító programnak (így például az operációs rendszernek), hogy miként járjon el. Utasíthatjuk, hogy vegye figyelmen kívül a hibát és folytassa a futtatási folyamatot (Ignore). Megkérhetjük, hogy jelenítsen meg egy üzenet ablakot az észlelt hibával, majd folytassa az indítást (Normal). Utasíthatjuk, hogy amennyiben nem a legutolsó ismert helyes konfiguráció fut, akkor indítsa újra a rendszert vele (Severe), végül megadhatjuk, hogy ha már fut az utolsó ismert helyes konfiguráció, akkor szakítsa meg az indítási folyamatot.

 

Most, hogy már tudjuk az alapokat, nézzük meg, miként kérdezhetjük le ezeket az információkat a SCM-től.

 

Kapcsolódás a SCM-hez

 

A kapcsolatot az OpenSCManager() API függvénnyel tudjuk megnyitni:

function OpenSCManager(lpMachineName, lpDatabaseName: PChar;

  dwDesiredAccess: DWORD): SC_HANDLE; stdcall;

A paraméterek jelentése:

Þ      lpMachineName. A kapcsolódáshoz először meg kell adnunk, hogy mely számítógéphez szeretnénk kapcsolódni. Ebből kitűnik, hogy a hálózatba kötött bármely géphez kapcsolódni tudunk, és távolról kezelni a rajtuk futó szolgáltatásokat. Ez egy olyan funkció, amit az alap Windows eszközökkel nem tudunk elérni! Nekünk viszont most lehetőségünk van egy olyan programot megírni, mely ezt lehetővé teszi. Ha a helyi géphez szeretnénk kapcsolódni, ezt a mezőt egy üres karakterlánccal kell kitölteni.

Þ      lpDatabaseName: A rendszer elméletileg több adatbázist tud kezelni, melyek a szolgáltatások beállításait tartalmazzák. Valójában csak egyetlen ilyen adatbázist tudunk elérni, az éppen aktívat. Ezt a paramétert tehát vagy üresen hagyjuk, vagy a SERVICES_ACTIVE_DATABASE konstanst adjuk meg, mint kettő jelentése ugyan az.

Þ      dwDesiredAccess: Ennek a paraméternek minden bitje egy olyan funkciót jelent, melyeket az alábbiakban ismertetek. Természetesen nem minden felhasználó férhet hozzá minden adathoz, és nem mindent lehet módosítani. Ebben a paraméterben adhatjuk meg, hogy milyen hozzáféréssel szeretnénk kapcsolódni a SCM-hez. Ha annak a felhasználónak, amely programunkat futtatja, nincs joga elvégezni bizonyos műveleteket (pl. nem állíthat le szolgáltatásokat), akkor az már a kapcsolódás során ki fog derülni.

A visszakapott érték egy kezelőszám, melyet a későbbiekben fogunk felhasználni. Ha valamilyen hiba történt, úgy azt 0 visszatérési értékkel jelzi a rendszer.

 

Na jó, most akkor jelzi a Windows, hogy hiba volt, de egy 0-ból még nem fogjuk tudni, hogy mi történt! Ennek kiderítésére használhatjuk a GetLastError() függvényt:

function GetLastError: DWORD; stdcall;

A rendszer minden szál számára elmenti az utolsó elvégzett művelet hibakódját. Számos Windows API függvény csak a hiba bekövetkezésének tényét jelzi számunkra és ezzel a függvénnyel tudjuk lekérdezni a tényleges hibakódot.

Ez még mindig szép és jó, de mint a legtöbb ember, én is kényelmes vagyok valamennyire, nem szeretek teljes kódtáblákat benyalni. Szerencsére erre is van megoldás, melyet most kivételesen a Delphi API-ja nyújt számunkra a SysUtils unit-ban:

function SysErrorMessage(ErrorCode: Integer): string;

Paraméterként csak át kell adnunk a GetLastError visszatérési értékét és egy olyan karakterlánccal tér vissza, mely a kódhoz tartozó hibaüzenetet tartalmazza, méghozzá az operációs rendszer nyelvén (angol Windowsnál angolul, magyarnál magyarul, stb.).

 

A kapcsolat lezárása

 

Ha egyszer kapcsolódtunk a SCM-hez, a folyamat végén le is kell zárni azt.

function CloseServiceHandle(hSCObject: SC_HANDLE): BOOL; stdcall;

Talán nem is igényel sok magyarázatot, az imént megkapott kezelőszámot kell csak megadnunk és készen is vagyunk.

 

Elérhető szolgáltatások feltérképezése

 

Most, hogy már képesek vagyunk kapcsolódni bármely gép adatbázisához, nézzük, hogyan készíthetünk egy listát az elérhető szolgáltatásokról.

function EnumServicesStatus(hSCManager: SC_HANDLE; dwServiceType,

  dwServiceState: DWORD; var lpServices: TEnumServiceStatus; cbBufSize: DWORD;

  var pcbBytesNeeded, lpServicesReturned, lpResumeHandle: DWORD): BOOL; stdcall;

Elsőként meg kell adnunk az OpenSCManager() által visszaadott kezelőszámot, majd lehetőség van a kapott listát szűrni. A dwServiceType az alábbi értékek bármely kombinációját veheti fel:

Þ      SERVICE_DRIVER: Olyan szolgáltatások, melyek meghajtó programokhoz tartoznak.

Þ      SERVICE_WIN32: Ez az a bizonyos „minden egyéb” kategória.

A dwServiceState paraméterrel szűkíthetjük a kört csak az éppen futó vagy nem futó szolgáltatásokra:

Þ      SERVICE_ACTIVE: Minden olyan szolgáltatást visszaad, mely nem a Leállítva (Stopped) állapotban van.

Þ      SERVICE_INACTIVE: Az éppen leállított állapotú szolgáltatások listáját kapjuk.

Þ      SERVICE_STATE_ALL: Ez a fenti kettő kombinációja.

Az így megadott szűrésekkel az lpServices paraméterben kapjuk vissza az elérhető szolgáltatások listáját. Ez egy ENUM_SERVICE_STATUS rekordokat tartalmazó tömb. Hogy hány rekord fér az általunk átadott tömbbe, azt a cbBufferSize paraméterben adjuk meg.

Ebből rögtön kitűnik, hogy nem biztos, hogy akkora méretű tömbnek foglalunk le helyet, amelyben minden szolgáltatás adata elfér. Ilyenkor a pcbBytesNeeded paraméterben kapjuk meg, hogy még mekkora tárterületre volna szükség a maradék lista elérésére és az lpServicesReturned-ben, hogy most éppen hány darab szolgáltatásról kaptunk adatokat. Annak érdekében, hogy az így megszakadt lista lekérését onnan tudjuk folytatni, ahol félbeszakadt, az lpResumeHandle-ben kapunk egy értéket. Ezt kell ugyanebbe a paraméterbe átadni a függvény következő hívásakor, így a már megkapott elemeket nem adja vissza újra. Annak a tényét, hogy van még vissza nem adott adat, a GetLastError-ral tudjuk lekérdezni. Ilyenkor az INVALID_MORE_DATA értéket kapjuk vissza.

Nézzük még meg az ENUM_SERVICE_STATUS rekord felépítését:

  ENUM_SERVICE_STATUS = record

    lpServiceName: PAnsiChar;

    lpDisplayName: PAnsiChar;

    ServiceStatus: TServiceStatus;

  end;

A mezők jelentése:

Þ      lpServiceName: ez egy belső azonosító karakterlánc. Ezt fogjuk felhasználni a későbbiek során, amikor egy konkrét szolgáltatást szeretnénk vezérelni.

Þ      lpDisplayName: a szolgáltatás megjelenítéshez használatos neve.

Þ      ServiceStatus: ez egy további rekord, mely az adott szolgáltatásról ad további információkat, úgy, mint jelenlegi állapota (fut, nem fut, él-e még), milyen parancsokat tud fogadni (innen tudhatjuk például meg, hogy lehet-e szüneteltetni vagy sem). Részletesen a következő részben foglalkozunk vele.

 

Egy példa

 

Tudom, hogy a fent leírtak kicsit töménynek tűnnek, ezért most készítsünk egy olyan programot, mely egy TListBox-ba elhelyezi a helyi gépen elérhető szolgáltatásokat.

Készítsünk egy új alkalmazást és dobjunk rá egy TListBox-ot, aminek a Sorted tulajdonságát állítsuk True-ra a rendezettség kedvéért. Dobjunk még egy TButton-t is a Form-ra, OnClick-jébe pedig a következőket írjuk:

procedure TForm1.Button1Click(Sender: TObject);

var

  scHandle : SC_HANDLE;

 

  st : TEnumServiceStatus;

  Buffer : array of TEnumServiceStatus;

  NumNeed, NeedBytes, Ret, Resume, ErrorCode : Cardinal;

  i : integer;

  Success : BOOL;

begin

  scHandle := OpenSCManager('',nil,SC_MANAGER_ALL_ACCESS);

  if scHandle = 0 then

    begin

      MessageDlg(SysErrorMessage(GetLastError),mtError,[mbOK],0);

      exit;

    end;

 

  ListBox1.Items.Clear;

  try

    Ret := 0;

    NeedBytes := 0;

    Resume := 0;   // Amikor először hívjuk az EnumServicesStatus-t, ezt 0-ra kell állítani

 

    Success := EnumServicesStatus(scHandle,SERVICE_WIN32,SERVICE_STATE_ALL,st,0,NeedBytes,Ret,Resume);

    ErrorCode := GetLastError;

 

    // Normális esetben ez ErrorCode ERROR_MORE_DATA lesz, mivel 0 hosszú tömböt adtunk át neki

    if ErrorCode = ERROR_MORE_DATA then

      begin

        // Kiszámoljuk, hogy mennyi szolgáltatásnak kell hely a bufferbe

        NumNeed := NeedBytes div SizeOf(TEnumServiceStatus) + 1;

        SetLength(Buffer,NumNeed);

 

        // Most már tényleg lekérjük a _teljes_ listát

        if EnumServicesStatus(scHandle,SERVICE_WIN32,SERVICE_STATE_ALL,Buffer[0],SizeOf(TEnumServiceStatus) * numneed,NeedBytes,Ret,Resume) then

          begin

            // Ha végeztünk, listázzuk az eredményt

            for i := low(buffer) to ret - 1 do

              if Assigned(buffer[i].lpServiceName) then

                ListBox1.Items.Add(buffer[i].lpDisplayName);

          end

          else

            MessageDlg(SysErrorMessage(GetLastError),mtError,[mbOK],0);

      end

      else

        if not Success then

          MessageDlg(SysErrorMessage(ErrorCode),mtError,[mbOK],0);

  finally

    if not CloseServiceHandle(scHandle) then

      MessageDlg(SysErrorMessage(GetLastError),mtError,[mbOK],0);

  end;

end;

Először felvesszük a kapcsolatot a helyi SCM-el. Ezt jelzi az üres string az OpenSCManager-ben. Ha bármi hiba van, akkor 0-t ad vissza. Ezt ellenőrizzük, és szükség esetén tájékoztatjuk a felhasználót.

Ha minden rendben volt, akkor töröljük a ListBox jelenlegi tartalmát és egy try..finally..end blokk következik. Erre azért van szükség, hogy a létrejött kapcsolatot mindenképpen lezárjuk, ha valamilyen hiba történik, akkor is.

A lista lekérdezését többféleképpen meg lehet írni. Lehet úgy, hogy mondjuk tízesével, vagy húszasával kérjük le őket egy fix hosszú tömbbe.

Egy másik - szerintem szebb :) – módszer, ha kicsit trükközünk a függvénnyel. Először egy nulla hosszú tömböt adunk át, így egyetlen elemet se kapunk vissza, megkapjuk viszont a teljes lista eléréséhez szükséges elemszámot. Pontosabban a szükséges tömb méretét, melyből egy egyszerű osztással visszaszámoljuk, hogy mennyi elemre van szükségünk. Ezt aztán egy nyitott tömbnek beállítjuk, és újra meghívjuk az EnumServicesStatus-t, de most már a megfelelő méretű tömbbel.

Amennyiben nem keletkezett hiba, feltöltjük a ListBox-ot a visszakapott rekordok lpDisplayName mezőjével. Ha készen vagyunk és megnéztük az eredményt, nézzük meg lista tartalmát, ha az lpServiceName mezővel töltjük fel. Ugye ez már kevésbé barátságos?

 

A következő hónapban megnézzük, miként kezelhetjük az egyes szolgáltatásokat. Lekérjük adataikat, megpróbáljuk futtatni vagy éppen leállítani őket.

 

Geiger Tamás info@gsc.hu