Index du site
Google

Le service sous Windows

Copie d'écran

L'archive zippée tec020.zip contient les fichiers de la page.

haut de page  Principe

Une 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.

haut de page  Service

Selon 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:

main

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);
}

ServiceMain

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):

GestionControle

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;
}

InitService

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);
}

haut de page  Processus

Dans 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:

haut de page  Implémentation

Ici 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.

Création et configuration du service

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:

Cette 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.

Arrêt et suppression du service

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.

Variantes

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).

haut de page  Output

Voici 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]

haut de page  Remarques

Le 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]

Copyright © 2008 B. Challier • légalcontact Valid XHTML 1.0 Valid CSS 2haut