|
L'archive zippée tec020.zip contient les fichiers de la page.
PrincipeUne application de type service (ou un service, pour faire plus court) est
particulière en ce sens qu'elle peut être
lancée automatiquement au démarrage de Windows, ou à partir
de l'interface des services ou encore par une
application utilisant les fonctions ad hoc (CreateService,
DeleteService, etc.). À ce propos, MSDN - voir l'Onglet Doc -
publie un exemple de code source complet d'une application qui crée et installe
un service.
Un service peut tourner même si aucun
utilisateur n'est connecté (loggé). Enfin dans le cas d'un service, la barre des
tâches ne contient pas, contrairement à une application normale, le traditionnel
bouton correspondant à un programme qui tourne.
Un service est donc intéressant parce qu'il peut tourner en arrière-plan, et parce qu'il ne demande pas d'interaction avec l'utilisateur. Nous allons maintenant détailler ce qui fait la particularité d'un service, puis nous nous pencherons sur un exemple de service qui récupère la liste des processus actifs.
ServiceSelon MSDN, le point d'entrée d'un service Windows est la fonction
ServiceMain. Or elle cohabite avec une fonction main
qui ressemble aussi à un point d'entrée d'application. En fait ServiceMain
tourne sur un thread (fil) séparé par rapport à main, à partir
de l'appel à la fonction RegisterServiceCtrlHandler.
Voici quelques extraits significatifs du code source de l'application:
Cette fonction se borne à initialiser la table décrivant le service avant de le connecter au Service Control Manager (SCM):
int main(int argc, char* argv[]) {
// Initialisons la table du service:
SERVICE_TABLE_ENTRY ServiceTable[2];
ServiceTable[0].lpServiceName = sNomApp;
ServiceTable[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain;
ServiceTable[1].lpServiceName = NULL;
ServiceTable[1].lpServiceProc = NULL;
// Connectons le thread principal du service au Service Control Manager (SCM):
StartServiceCtrlDispatcher(ServiceTable);
return(0);
}
Il s'agit du gros morceau de l'application. Tout d'abord le statut du service est paramétré comme étant du Windows 32 bits sur le point de démarrer, acceptant de réagir aux requêtes (ou contrôles) d'arrêt, de coupure de Windows, de mise en pause et de continuation:
// Statut du service: StatutService.dwServiceType = SERVICE_WIN32; StatutService.dwCurrentState = SERVICE_START_PENDING; // Les requêtes acceptées par le service: StatutService.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_PAUSE_CONTINUE; StatutService.dwWin32ExitCode = NO_ERROR; StatutService.dwServiceSpecificExitCode = 0; StatutService.dwCheckPoint = 0; StatutService.dwWaitHint = 0;
Le gestionnaire de contrôle du service est ensuite enregistré:
hStatut = RegisterServiceCtrlHandler(sNomApp, (LPHANDLER_FUNCTION)GestionControle);
Puis le service voit son statut réglé sur "en cours d'exécution":
StatutService.dwCurrentState = SERVICE_RUNNING; SetServiceStatus(hStatut, &StatutService);
Il est d'ailleurs intéressant de suivre les valeurs que prend, au cours du
programme, le membre dwCurrentState de la stucture
SERVICE_STATUS (déclarée comme variable globale dans le fichier
d'en-tête):
SERVICE_START_PENDING, tout au début;SERVICE_RUNNING, après initialisation ou après une pause;SERVICE_STOPPED, en cas d'erreur ou sur un arrêt prématuré
- du fait de l'utilisateur par exemple;SERVICE_PAUSED lorsque le service est mis en pause.Le gestionnaire de contrôle fonctionne à la manière d'une fonction callback gérant les messages Windows:
void GestionControle(DWORD dwRequete) {
switch(dwRequete) {
case SERVICE_CONTROL_STOP:
case SERVICE_CONTROL_SHUTDOWN:
EcrireLog(_T("Service %s arrêté avant la fin."), sNomApp);
StatutService.dwWin32ExitCode = NO_ERROR;
StatutService.dwCurrentState = SERVICE_STOPPED;
break;
case SERVICE_CONTROL_PAUSE:
EcrireLog(_T("Service %s en pause."), sNomApp);
StatutService.dwWin32ExitCode = NO_ERROR;
StatutService.dwCurrentState = SERVICE_PAUSED;
break;
case SERVICE_CONTROL_CONTINUE:
EcrireLog(_T("Service %s reparti."), sNomApp);
StatutService.dwWin32ExitCode = NO_ERROR;
StatutService.dwCurrentState = SERVICE_RUNNING;
break;
default:
break;
}
// Détermine le statut en cours:
SetServiceStatus (hStatut, &StatutService);
return;
}
Cette fonction d'initialisation du service est ici réduite à sa plus simple
expression. Elle sert à tester le fonctionnement de l'écriture dans le fichier
de log, car tout au long de son fonctionnement le service devra y écrire
des informations (la fonction EcrireLog est décrite et
téléchargeable dans l'onglet Utile):
int InitService(void) {
int iResult;
// EcrireLog renvoie 1 si OK, zéro sinon:
iResult = EcrireLog(_T("\nDémarrage du service %s."), sNomApp);
return(iResult);
}
ProcessusDans cette page nous voyons un exemple de service qui, à intervalle régulier,
récupère la liste des processus qui tournent et la recopie dans un fichier de
log se trouvant dans le même répertoire que le service. Pour ce faire, notre
petit programme fait appel à une API appelée Process Status API ou
PSAPI.
Le makefile en tient compte (option -lpsapi) ainsi que le
fichier d'en-tête qui inclut psapi.h.
La liste des processus actifs est obtenue en routine sous Windows
par le Gestionnaire de Tâches (Task Manager) dont l'exécutable est, par exemple
sur mon poste, taskmgr.exe qui se trouve dans le
répertoire C:\WINDOWS\SYSTEM32\ - bien sûr, Task Manager
donne beaucoup d'autres informations.
La liste des processus actifs est indépendante du type de programme (un service),
mais l'association des deux permet de surveiller en toile de fond ce qui tourne à
un instant donné sur une machine donnée.
Dans la fonction ServiceMain, tant que le service est en cours
d'exécution (SERVICE_RUNNING), nous trouvons une boucle exécutée
un certain nombre de fois, avec un temps d'attente (fonction Sleep):
for (iCompteur=0; iCompteur<NB_TOURS; iCompteur++) {
EcrireLog(_T("Service %s: iCompteur vaut %d"), sNomApp, iCompteur);
if (EnumProcesses(aListeProc, NB_PROC, &dwRetOctets)) {
for (i=0; i<(dwRetOctets / sizeof(DWORD)); i++) {
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, aListeProc[i]);
if (NULL != hProcess) {
if (EnumProcessModules(hProcess, &hModule, sizeof(hModule), &dwBesoin)) {
GetModuleBaseName(hProcess, hModule, sNomProcess, sizeof(sNomProcess) / sizeof(TCHAR));
}
EcrireLog(_T("- Nom du process (pid %u): \'%s\'"), aListeProc[i], sNomProcess);
}
CloseHandle(hProcess);
}
} else {
EcrireLog(_T("! EnumProcesses renvoie FALSE"));
}
Sleep(dwAttente);
}
Regardons d'un peu plus près les fonctions de processus qui sont appelées dans cette partie du programme:
EnumProcesses remplit un tableau d'entiers longs non
signés (DWORD) avec la liste des identifiants de processus actifs (pid);OpenProcess ouvre en lecture chaque processus et renvoie un
handle sur ce processus;EnumProcessModules renvoie un handle sur le module du
processus;GetModuleBaseName écrit le nom du module dans une chaîne de
caractères;CloseHandle libère le handle du processus étudié.
ImplémentationIci nous supposons que l'exécutable tec020.exe, notre service, se trouve dans
le répertoire c:\monservice\
(que bien entendu il nous faut créer au préalable) du disque dur du
poste client.
Le makefile fourni dans le zip permet de compiler directement dans ce répertoire -
sinon il faut simplement y recopier l'exécutable après compilation.
Sous Invite de Commandes, nous créons et configurons le service au moyen du fichier batch dans le zip (tec020cfg.bat), ou bien une commande à la fois, en se rappelant que l'espace après le signe égal (binpath, start, etc.) est obligatoire:
sc create tec020
binpath= c:\monservice\tec020.exe
displayname= "Service tec020 (www.toutenc.com)"sc description tec020 "Service de test.
Recopie la liste des processus actifs dans un fichier de log."sc config tec020 start= demandCette série de commandes fait appel au Service Controller
de Windows pour incorporer notre service dans la liste des programmes candidats,
par l'ajout d'une clé dans le registre de Windows.
Ensuite nous accédons au panneau des services, en cliquant Démarrer > Panneau de Configuration > Outils d'Administration > Services. De là il est possible de lancer le service manuellement, de le mettre en pause, de l'arrêter ou de le redémarrer.
Pour en savoir plus sur ce service ou sur n'importe quel autre, il suffit de faire un clic droit en choisissant Propriétés (idem par les boutons ou le menu). Les onglets Général et Dépendances sont intéressants.
Le service tel qu'il est compilé s'arrête tout seul après deux
itérations espacées de dix secondes.
Depuis le panneau des services décrit ci-dessus, il est
possible de forcer un service à s'arrêter, mais il reste disponible.
Pour supprimer notre service du registre, nous pouvons exécuter,
toujours en Invite de Commandes: sc delete tec020.
Pour créer un service (un mouchard par exemple) à lancement automatique
et dépendant du fonctionnement du service "Station de travail"
(lanmanworkstation), nous pouvons le configurer par
sc config tec020 start= auto depend= lanmanworkstation.
Dans ce cas, une fois qu'il sera devenu inutile, il faudra penser à
le re-configurer en manuel ou à le supprimer du registre
(sc delete tec020) - sinon il va s'activer à chaque
(re)démarrage de Windows.
Selon les besoins, le log (bien sûr il pourra être mieux caché que
dans le répertoire c:\monservice\ !)
pourra contenir d'autres informations,
très peu s'il s'agit juste de signaler que l'ordinateur a été démarré
tel jour à telle heure. Si nous savons coder des socket
TCP/IP
en C pour Windows (bibliothèque WinSock 2) le service peut devenir
communiquant, par exemple en envoyant un email
(SMTP).
OutputVoici un exemple de contenu du fichier de log. La session comporte un démarrage manuel depuis le panneau des services, une pause, continuation, un redémarrage puis un arrêt normal. Les parties (...) sont retirées:
Démarrage du service tec020. [2008-04-21 22:22:43] Nom de l'utilisateur: (...) [2008-04-21 22:22:43] Nom de l'ordinateur: (...) [2008-04-21 22:22:43] Service tec020: iCompteur vaut 0 [2008-04-21 22:22:43] - Nom du process (pid 640): 'smss.exe' [2008-04-21 22:22:43] - Nom du process (pid 688): 'csrss.exe' [2008-04-21 22:22:43] - Nom du process (pid 712): 'winlogon.exe' [2008-04-21 22:22:43] (...) - Nom du process (pid 1104): 'firefox.exe' [2008-04-21 22:22:44] (...) - Nom du process (pid 3536): 'tec020.exe' [2008-04-21 22:22:44] Service tec020 en pause. [2008-04-21 22:22:48] Service tec020 reparti. [2008-04-21 22:22:52] Service tec020: iCompteur vaut 1 [2008-04-21 22:22:54] (...) - Nom du process (pid 1112): 'thunderbird.exe' [2008-04-21 22:22:54] - Nom du process (pid 1484): 'livesrv.exe' [2008-04-21 22:22:54] - Nom du process (pid 1684): 'vsserv.exe' [2008-04-21 22:22:54] (...) - Nom du process (pid 3536): 'tec020.exe' [2008-04-21 22:22:54] Service tec020 arrêté avant la fin. [2008-04-21 22:22:56] Démarrage du service tec020. [2008-04-21 22:22:58] Nom de l'utilisateur: (...) [2008-04-21 22:22:58] Nom de l'ordinateur: (...) [2008-04-21 22:22:58] Service tec020: iCompteur vaut 0 [2008-04-21 22:22:58] (...) Service tec020: iCompteur vaut 1 [2008-04-21 22:23:08] (...) Arrêt normal du service tec020. [2008-04-21 22:23:18]
RemarquesLe Service Controller est, comme nous l'avons vu, accessible par le
panneau de contrôle des services. L'exécutable
sc (utilisé plus haut pour create,
config ou description) permet de faire la même chose et
beaucoup plus: il prend comme arguments start, pause, stop,
continue, etc.
Tapez sc ou sc config dans l'Invite de Commandes
pour voir la référence en ligne.
Cette page est redevable à Yevgeny Menaker pour son lumineux article sur les services sous Windows, disponible en ligne chez DevX.com et dont voici le lien. MSDN a aussi été une précieuse source d'inspiration grâce à un article intitulé "Enumerating all processes".
[màj 22 avril 2008]