Lancer une application dès le démarrage du système

Lorsqu'un programme doit démarrer dès l'initialisation du système, sans qu'un utilisateur n'ait besoin de se connecter pour le lancer, il est indispensable de respecter certaines règles pour simplifier la mise en service et éviter les problèmes de sécurité. Nous examinerons dans cet article les principaux points importants à envisager, en détaillant les différents types d'interfaces utilisateur que l'application peut mettre en oeuvre et les configurations qui en découlent.

Introduction

Le développement de Linux permet de multiplier le nombre de stations de travail de type Unix en employant du matériel peu coûteux, dont les performances et la fiabilité connaissent néanmoins une amélioration constante. De plus, l'engouement du public pour Linux met à la disposition des entreprises un nombre important d'administrateurs système et développeurs compétents (c'est peut-être finalement cet engouement du public qui manquait aux projets BSD libres pour une large diffusion dans le monde professionnel).

On peut ainsi rencontrer de plus en plus fréquemment, surtout dans les environnements de production, des sites composés d'une multitude de machines, chacune étant dédiée à une seule et unique tâche. Telle station contient un système d'enregistrement et de statistiques, telle autre un programme de visualisation temps-réel des données, telle autre enfin un outil de diagnostique des mesures capable d'alerter un opérateur en cas de danger potentiel. Une machine dédiée peut également servir à superviser les autres stations et le réseau lui-même, détectant les pannes, les saturations ou les absence de trafic. Dans d'autres environnements, certains ordinateurs seront réservés pour la journalisation des connexions externes, et d'autres serviront à mettre en lumière les tentatives d'intrusion.

Pour simplifier la tâche de l'administrateur et des utilisateurs, il est souhaitable que l'application principale d'une machine soit lancée automatiquement lors du démarrage de cette dernière, sans avoir besoin de passer par une phase de connexion utilisateur / mot de passe. Il faut quand même observer quelques règles de prudence concernant la sécurité du système pour éviter qu'une maladresse non intentionnelle dégrade la configuration de la station.

Nous supposerons donc que l'on dispose d'une application qui doit être installée pour démarrer dès l'initialisation de la machine. En fonction de l'interface utilisateur la configuration sera différente, aussi examinerons-nous plusieurs cas. Pour des raisons de sécurité nous pourrons aussi être amenés à lancer l'application indirectement par le biais d'un petit programme gérant les identifiants de l'utilisateur.

Initialisation du système

Sans entrer vraiment dans les détails, nous devons parcourir rapidement les diverses étapes du démarrage d'une machine pour bien voir où notre application devra s'insérer.

Lorsqu'un ordinateur est mis sous tension, un programme se trouvant en mémoire morte est exécuté. Il vérifie l'état de la mémoire vive, initialise les ports d'entrée-sortie puis, sous contrôle du paramétrage inscrit dans le setup, charge et lance le code se trouvant sur les premiers secteur du disque dur, d'une disquette, voire d'un CD Rom. Sur la plupart des stations Linux il s'agit du programme de lancement Lilo, mais on peut aussi rencontrer une image du noyau directement inscrite sur une disquette. Quoiqu'il en soit, l'initialisation du système d'exploitation commence véritablement à ce moment avec le chargement en mémoire du noyau, et son démarrage. Celui-ci détecte et initialise les périphériques, prépare ses structures de données, puis lance un premier processus, init.

Le processus init, dont le PID vaut 1 par convention, consulte le fichier /etc/inittab, et lance les applications qui y sont mentionnées. Un système Linux dispose de plusieurs niveaux d'exécution, qui correspondent à des configurations différentes des applications démarrées. Le niveau d'exécution par défaut est indiqué en début de fichier, par exemple :

  # Démarrage par défaut au niveau 5
  id:5:initdefault:

Sur la plupart des distributions Linux, le niveau d'exécution 3 correspond à un démarrage avec tous les utilitaires standards, mais en mode texte (console) alors que les niveaux 4 ou 5 correspondent au démarrage graphique avec X-Window. On rencontre alors une ligne du genre :

  # Lancer XDM (X-Window Display Manager) dans les niveaux 4 et 5
  x:45:respawn:/usr/X11R6/bin/xdm -nodaemon

Dans le fichier /etc/inittab se trouvent également les lancement des démons système (comme sendmail, crond ou inetd) ainsi que les utilitaires de la famille getty qui gèrent la connexion sur des consoles virtuelles en mode texte.

Le fichier /etc/inittab sera donc le point central de notre configuration, et mérite que l'on s'arrête quelques instants sur sa composition, qui est relativement simple. Les lignes commençant par le caractère # représentent des commentaires, qui sont ignorés. Sinon, les autres lignes ont la structure suivante :

  identifiant:niveaux:type_action:commande

L'identifiant doit être unique. Il s'agit d'un ou deux caractères qui seront essentiellement utilisés pour diagnostiquer les problèmes lors de la lecture des fichiers-journaux du système. Le second champ indique la liste des niveaux d'exécution pour lesquels la ligne s'applique. Certaines lignes ont un rôle particulier et laissent ce champ vide, mais cela ne nous concernera pas ici. Le type d'action indiqué en troisième position permet de qualifier la ligne. Les types les plus intéressants pour nous sont :

  • once : Exécuter la commande se trouvant dans le dernier champ une et une seule fois, lors de l'entrée au niveau d'exécution considéré (au démarrage normalement).

  • respawn : Lancer la commande indiquée en quatrième position, puis la redémarrer à chaque fois qu'elle se terminera. Il s'agit généralement du comportement le plus intéressant, puisque l'application redémarrera si l'utilisateur l'arrête par erreur, ou si elle s'interrompt pour une raison quelconque.

  • wait : Lancer la commande lorsqu'on entre dans le niveau d'exécution indiqué (comme avec once), mais attendre que le processus se termine avant de passer à la suite. En général cette action est utilisée pour démarrer des démons système qui vont basculer d'eux-mêmes à l'arrière-plan, en rendant la main à init.

    D'autres types d'action existent, comme initdefault qui spécifie le niveau d'exécution au démarrage, sysinit qui paramètre le lancement des applications durant le boot, ou encore powerfail ou powerokwait qui gèrent les coupures et reprises de courant grâce à un système d'alimentation protégée.

    La partie commande de la ligne devra comporter le chemin d'accès complet jusqu'au fichier exécutable à lancer, suivi de ses éventuels arguments.

    Interface utilisateur

    En fait, la configuration que nous adopterons pour lancer automatiquement notre application au démarrage du système dépendra essentiellement de son interface utilisateur, qui nous indiquera où et comment placer notre programme. Nous allons étudier en premier lieu le cas des applications n'ayant pas de contact avec l'utilisateur. Nous examinerons ensuite les situations où le programme dispose d'une interface en mode texte, puis en mode graphique.

    Application sans interaction avec l'utilisateur

    Le cas le plus courant d'application lancée directement au démarrage du système est celui des démons. Il s'agit de programme s'exécutant à l'arrière-plan, sans terminal de contrôle, et chargés pour la plupart de travaux administratifs comme l'exécution de jobs différés (cron), le routage et la distribution du courrier électronique (sendmail), la gestion des connexions réseau (inetd) ou l'impression en tâche de fond (lpd).

    Un tel programme doit respecter un certain nombre de règles :

  • basculer à l'arrière-plan, même s'il a été lancé au premier-plan par le shell;
  • créer une nouvelle session de processus pour ne pas avoir de terminal de contrôle;
  • fermer tous les descripteurs de fichiers dont il peut avoir hérité de son processus père;
  • revenir à la racine du système de fichiers pour ne pas risquer de bloquer une partition que l'administrateur ne pourrait pas démonter.

    Ces opérations sont réalisées dès l'initialisation du programme par quelques lignes de code. Par exemple le morceaux de programme suivant lance sous forme de démon l'application proprement dite, représentée par la routine mon_application() :

      /* demon.c */
      
      #include <limits.h>
      #include <unistd.h>
    
      int mon_application (int argc, char * argv []);
    
        int
      main (int argc, char * argv [])
      {
        int fd;
        if (fork () != 0)
          exit (0);
        setsid();
        for (fd = 0; fd < OPEN_MAX; fd ++)
          close (fd);
        chdir ("/");
    
        return (mon_application (argc, argv));
      }
    

    Démarrage du logiciel

    Pour lancer l'exécution du programme, nous allons agir dans /etc/inittab. Il s'agit de la méthode la plus générale, fonctionnant sur toutes les distributions de Linux, et même sur la majeure partie des Unix. Certains administrateurs rechignent à modifier directement ce fichier, en préférant agir sur les scripts qu'il lance automatiquement. On peut ainsi ajouter les commandes que nous voulons dans le script rc.local par exemple. C'est une bonne méthode car elle évite de surcharger un fichier système en y apportant des modifications à chaque fois qu'un démon est ajouté. Toutefois nous nous plaçons ici dans le cas d'une machine dédiée essentiellement à une application donnée, et l'intervention sur le fichier /etc/inittab se justifie très bien si elle est limitée à l'installation de ce logiciel. L'application que nous lançons passe automatiquement à l'arrière-plan. La commande utilisée pour le démarrage va donc se terminer presque immédiatement (dans le exit(0) suivant le fork()). Il n'est donc pas question de demander un démarrage de type respawn, car init essayerait de la relancer tout de suite, bouclant ainsi sur cette ligne pendant quelques secondes au bout desquelles il abandonnerait en différant le lancement pour réessayer au bout de quelques minutes. En attendant, la boucle respawn aurait lancé un nombre important de démons en parallèle, surchargeant le système.

    Pour démarrer un démon, on préfère en général un lancement de type wait, init attendant que le processus ait basculé à l'arrière-plan pour continuer son travail. On ajoute donc dans /etc/inittab une ligne du type :

        lm:3:wait:/usr/local/bin/mon_demon
    

    Les caractères du premier champ sont choisis arbitrairement de manière à former un identificateur unique dans le fichier. Le niveau d'exécution indiqué ensuite est celui auquel le démon sera actif. Sauf exception, il s'agira du niveau d'exécution indiqué dans la ligne initdefault au début du fichier.

    Transmission de messages administratifs

    L'application n'ayant pas de possibilité de dialoguer avec l'utilisateur par l'intermédiaire du terminal, elle doit implémenter son propre mécanisme pour interagir avec son environnement. Selon les cas, elle peut employer des sockets, des tubes nommés, des fichiers et des signaux, tout cela au gré du programmeur ou plutôt du concepteur du logiciel. En revanche, pour pouvoir transmettre des informations de diagnostique, indispensables pour la mise au point et le débogage du système, il faut disposer d'un mécanisme remplaçant l'écriture de message sur la sortie d'erreur.

    Nous employons pour cela les possibilités offertes par le démon syslogd. Il s'agit d'un système de journalisation qui centralise tous les messages d'erreur ou de diagnostique émis par les applications l'invoquant, et qui permet d'enregistrer dans un fichier, d'afficher à l'écran, d'envoyer par courrier électronique, ou d'imprimer les traces d'exécution. La configuration du démon syslogd lui-même se paramètre à l'aide du fichier /etc/syslog.conf.

    Pour le programmeur, l'utilisation de syslog est très simple, puisqu'elle se réduit à l'invocation de trois routines de la bibliothèque C, déclarées dans <syslog.h>. Ces fonctions sont les suivantes :

  • void openlog (char *ident, int option, int type) : cet appel facultatif au début du programme initialise le fonctionnement des routines. On transmettra en premier argument le nom du programme, ce qui permettra de l'identifier dans les fichiers de journalisation, en second argument la constante symbolique LOG_PID qui permettra l'ajout systématique du numéro de processus dans les traces d'exécution. En dernier argument on identifiera le type d'application avec la constante LOG_USER. On trouvera plus de détail dans la page de manuel openlog(3). Si on n'invoque pas openlog(), une initialisation par défaut aura lieu lors de la première journalisation.

  • void closelog ( void) : appelée facultativement en fin de programme, cette routine ferme la communication avec le démon syslogd.

  • void syslog ( int priorite, char * format...) : cette routine fonctionne un peu comme printf(), en utilisant une chaîne de format pour présenter des données. Le premier argument représente l'urgence du message. Les valeurs les plus courantes sont LOG_ERR en cas d'erreur grave (par exemple l'application risque de s'arrêter), LOG_INFO pour transmettre un message significatif mais sans danger (par exemple une connexion vient d'être établie), et LOG_DEBUG pour une information utile uniquement en débogage (par exemple allocation de 1024 octets).

    La configuration permet d'orienter les messages à sa guise en fonction de leur provenance et de leur urgence. Aussi n'hésiterons-nous pas à transmettre des avertissements pour toutes les situations significatives. Même en fonctionnement normal, il est très commode pour l'administrateur de s'assurer de la bonne santé d'un système fonctionnant de manière autonome en jetant un coup d'oeil dans les fichiers de logs comme /var/log/messages.

    L'utilisation du système syslog n'est pas réservée aux applications en langage C. Il existe un utilitaire nommé logger qui offre une interface pour les scripts shell. On l'utilisera souvent avec les options :

  • -t nom : indiquant le nom du programme, afin de faciliter l'exploration des fichiers de journalisation;

  • -i : qui réclame l'inscription du PID dans les lignes de message;

  • -p priorité : la priorité est indiquée sous une forme particulière : type.urgence. Le type sera généralement user et l'urgence sera err, info ou debug.

    Il est ensuite suivi du message proprement dit. Voici un exemple :

      #! /bin/sh
      # log.sh exemple d'invocation de syslog
    
      logger -i -t $0 -p user.err "Erreur grave"
      
      logger -i -t $0 -p user.info "Information utile"
    
      logger -i -t $0 -p user.debug "Message de déboguage"
    

    Sur la plupart des systèmes, les messages de déboguage ne sont pas enregistrés. Nous allons donc modifier notre configuration dans /etc/syslog.conf pour les diriger vers un nouveau fichier, et demander à syslogd de relire sa configuration en lui envoyant un signal SIGHUP. La ligne ajoutée dans /etc/syslog.conf est :

      user.=debug   /var/log/debug
    

    Voici un exemple d'utilisation de notre script :

    # killall -HUP syslogd
    # ./log.sh
    # tail /var/log/messages
    [...]
    Nov 13 16:34:15 venux syslogd 1.3-3: restart.
    nov 13 16:34:29 venux ./log.sh[2859]: Erreur grave
    nov 13 16:34:29 venux ./log.sh[2860]: Information utile
    # tail /var/log/debug
    nov 13 16:34:29 venux ./log.sh[2861]: Message de déboguage
    #
    

    Nous avons donc vu comment un programme sans interface utilisateur peut transmettre quand même des informations administratives au système. Ce mécanisme est aussi largement utilisé dans des programmes interactifs, quand un message doit être dirigé vers l'administrateur système et non pas vers l'utilisateur final.

    Limitation des privilèges

    Un programme lancé par le processus init s'exécute automatiquement sous l'identité root. Pour un démon tournant à l'arrière-plan, et dont les communications avec les utilisateurs sont soigneusement contrôlées, cela ne pose pas de gros problème. Toutefois lorsque l'application peut être manipulée par un utilisateur quelconque, comme nous le rencontrerons dans les logiciels étudiés plus loin, un véritable danger se présente.

    Sans même mettre en doute les intentions bienveillantes de l'utilisateur, une simple maladresse sous l'identité root peut être catastrophique; imaginez par exemple que l'on sauvegarde les statistiques du mois en cours dans /etc/passwd, /vmlinuz, ou /dev/hda.

    Il faut donc arriver à restreindre les privilèges de l'application. Pour cela on peut dans certains cas utiliser les bits Set-UID et Set-GID du fichier exécutable afin que le programme fonctionne automatiquement sous l'identité du propriétaire et du groupe du fichier (que l'on donnera à un utilisateur lambda sans privilège). Ce mécanisme peut parfois être suffisant, mais deux inconvénients se présentent quand même :

  • Le noyau n'honore pas les bits Set-UID et Set-GID sur les fichiers scripts pour des raisons de sécurité. Naturellement, cette restriction est en principe destinée à protéger le système dans le cas contraire, où les bits SetUID et Set-GID augmentent les privilèges du processus. Malheureusement la limitation est également valable dans notre situation, où l'on essaye de diminuer le niveau de privilèges. Nous ne pourrons donc pas utiliser ce moyen pour traiter les fichiers scripts. Notez que cela concerne pratiquement tous les scripts (Perl faisant exception grâce à un artifice basé sur un programme binaire Set-UID) et pas seulement le shell.

  • Lorsqu'un programme exécutable a son bit Set-UID à 1, il disposera d'un UID effectif correspondant au propriétaire du fichier. Cet UID effectif sera employé pour toutes les vérifications d'autorisation. Mais il continuera à disposer d'un UID réel correspondant à celui de l'utilisateur qui l'a lancé, ceci à des fins d'identification des actions. Un fichier Set-UID lambda démarré par root aura donc un UID effectif lambda et un UID réel root. Les vérifications d'accès aux fichiers du système se faisant avec l'UID effectif, les privilèges sont bien descendus. Mais un autre problème se pose : le processus est capable à tout moment de reprendre en UID effectif la valeur de son UID réel (root). Ce qui signifie que le programme va devenir une cible de choix pour les attaques de sécurité du type débordements de buffers.

    Il nous faut donc disposer d'un moyen de diminuer les privilèges des deux types d'UID, et de lancer des scripts avec ces mêmes modifications.

    Nous allons écrire un programme qui prend en argument une chaîne de caractères correspondant à un fichier exécutable suivi d'éventuels arguments. Ce processus va modifier ses UID et GID effectifs et réels pour correspondre à ceux du fichier exécutable visé, puis va l'invoquer avec la commande system().

    Ce programme ne dispose pas personnellement de privilèges particuliers, car il n'est conçu que pour en perdre. Ainsi il n'a d'intérêt que lorsqu'il est invoqué par root pour faire démarrer un programme appartenant à un utilisateur lambda. Comme l'invocation sera figée dans des fichiers système sous le contrôle unique de l'administrateur, ce programme ne peut en aucun cas servir à exploiter des failles de sécurité. On peut donc sans risque utiliser la fonction system() si dangereuse dans d'autres circonstances (programmes Set-UID root par exemple).

        /* lanceur.c */
        #include <stdlib.h>
        #include <string.h>
        #include <syslog.h>
        #include <unistd.h>
        #include <sys/stat.h>
    
        void
    set_uid (const char * fichier)
    {
        struct stat etat;
        char *      executable;
        char *      blanc;
        /* Copie du nom du fichier pour pouvoir le modifier */
        if ((executable = (char *) malloc (strlen
        (fichier) + 1)) == NULL) {
            syslog (LOG_ERR, "Mémoire manquante");
            exit (1);
        }
        strcpy (executable, fichier);
        /* Remplacement du premier blanc ou tabulation par un \0 */
        /* Ceci permet de supprimer les éventuels arguments */
        blanc = strchr (executable, ' ');
        if (blanc != NULL)
            blanc [0] = '\0';
        blanc = strchr (executable, '\t');
        if (blanc != NULL)
            blanc [0] = '\0';
     /* Lecture des informations concernant le fichier */
        if (stat (executable, & etat) != 0) {
            free (executable);
            syslog (LOG_ERR, "Fichier %s inaccessible",
            executable);
            exit (1);
        }
        free (executable);
        /* On fixe les UID/GID effectifs et réels */
        setregid (etat . st_gid, etat . st_gid);    
        setreuid (etat . st_uid, etat . st_uid);
    }
    
        int
    main (int argc, char * argv [])
    {
        openlog (argv [0], 0, LOG_USER);
    
        if (argc != 2) {
     syslog (LOG_ERR, "Syntaxe : %s commande", argv [0]);
            exit (1);
        }
    
        /* Utilisation des UID et GID du fichier exécutable */
        set_uid (argv [1]);
    
        /* Lancement du programme */
        system (argv [1]);
        return (0);
    }
    

    L'exécution suivante montre bien que lorsque root demande le lancement d'un fichier exécutable appartenant à un autre utilisateur, les UID et GID sont bien modifiés de telle manière que le retour à l'UID root nécessite la saisie du mot de passe (ce qui n'aurait pas été le cas si les UID / GID réels n'étaient pas modifiés).

      $ cc lanceur.c -o lanceur -Wall
      $ chmod +s lanceur
      $ ls -l lanceur
      -rwsrwsr-x  1 ccb  ccb   15680 Nov 16 11:21 lanceur
      $ cp /bin/sh .
      $ ls -l sh
      -rwxr-xr-x  1 ccb  ccb  373176 Nov 16 11:21 sh
      $ su
      Password:
      # ./lanceur ./sh
      bash$ whoami
      ccb
      bash$ su
      Password:
      su: incorrect password
      bash$ exit
      # exit
      $
    

    Notez que si l'on doit transmettre des arguments à la commande lancée, il faut encadrer le tout par des guillemets ainsi :

      $ lanceur "/usr/local/bin/mon_application et des arguments"
    

    La ligne de lancement dans le fichier /etc/inittab sera donc finalement modifiée ainsi :

      lm:3:wait:/usr/local/bin/lanceur "/usr/local/bin/mon_demon arg1 arg2"
    

    Application avec une interface en mode texte

    Le cas d'une application disposant d'une interface utilisateur en mode texte est intéressant car nous allons devoir gérer nous-même l'affection d'un terminal virtuel. Nous supposons que le but du système est de faire tourner automatiquement un logiciel employant une interface sur un terminal texte, soit avec les flux d'entrées-sorties standards, soit en employant une bibliothèque plus élaborée comme ncurses. Nous voici donc pratiquement revenu au temps du bon vieux Dos. Cette piste n'est d'ailleurs pas totalement incongrue, et pour faire tourner une application ne nécessitant pas la mise en oeuvre de toute la puissance d'un système Unix ni de toutes les possibilités d'interface réseau, le projet Freedos peut être très intéressant en permettant à une application de fonctionner à partir d'une disquette (donc sur des systèmes embarqués sans disque dur).

    Notre application va devoir s'attribuer un terminal texte à la manière des utilitaires de la famille getty (mingetty, mgetty, agetty, etc.) Cela s'obtient en ouvrant un pseudo-fichier /dev/ttyX, puis en dupliquant le descripteur obtenu pour l'affecter aux entrées et sorties standards. Le code nécessaire est le suivant :

        void
    get_tty (const char * tty)
    {
        int    fd;
        /* Ouverture du fichier indiqué */
        if ((fd = open (tty, O_RDWR, 0)) <0) {
        syslog (LOG_ERR, "Impossible d'ouvrir %s", tty);
            exit (1);
        }
        /* Vérification qu'il s'agit bien d'un terminal */
        if (! isatty (fd)) {
        syslog (LOG_ERR, "%s n'est pas un terminal", tty);
            exit (1);
        }
        /* Duplication du descripteur sur stdin, stdout, et stderr */
        if ((dup2 (fd, 0) ><0) || (dup2 (fd, 1) ><0) ||
        (dup2 (fd, 2) ><0)) {
            syslog (LOG_ERR, "Erreur dans dup2()");
            exit (1);
        }
        close (fd);
    }
    

    Cette routine est appelée en lui passant en argument le nom du pseudo-fichier /dev/ttyX désiré. Pour que l'ouverture réussisse, il faut que le programme s'exécute sous l'identité root. Le plus simple est donc de réaliser l'ouverture avant de modifier les UID et GID comme nous l'avons fait plus haut. Le programme de lancement est alors modifié pour recevoir en premier argument le nom du terminal à ouvrir.

        /* lanceur_texte.c */
        #include <fcntl.h>
        #include <stdlib.h>
        #include <string.h>
        #include <syslog.h>
        #include <unistd.h>
        #include <sys/stat.h>
    
        void
    set_uid (const char * fichier)
    {
    
        /* Même code que dans lanceur.c */
    }
    
        void
    get_tty (const char * tty)
    {
        /* Code présenté ci-dessus */
    }
    
        int
    main (int argc, char * argv [])
    {
        openlog (argv [0], 0, LOG_USER);
    
        if (argc != 3) {
         syslog (LOG_ERR, "Syntaxe : %s tty fichier",
         argv [0]);
            exit (1);
        }
        /* Ouverture de la console indiquée en premier argument */
        get_tty (argv [1]);
    
        /* Utilisation des UID et GID du fichier exécutable */
        set_uid (argv [2]);
    
        /* Lancement du programme */
        system (argv [2]);
        return (0);
    }
    

    Le démarrage du programme va se faire en principe sur la première console virtuelle, /dev/tty1, avec le niveau d'exécution 3 par défaut. Il va donc falloir remplacer le lancement de getty sur cette console. Il est probable que dans votre fichier /etc/inittab vous rencontriez l'une des lignes suivantes :

    1:2345:respawn:/sbin/mingetty tty1
    
    1:2345:respawn:/sbin/mgetty -rb tty1
    

    Il va falloir supprimer cette ligne, et la remplacer par :

    1:3:respawn:/usr/local/bin/lanceur_texte /dev/tty1 "/usr/local/bin/mon_appli"
    

    Le niveau d'exécution sera adapté à celui par défaut. Ici l'application ne passe pas en arrière-plan, contrairement aux démons que nous avons vus plus haut, aussi peut-on employer un lancement de type respawn qui redémarrera le logiciel si l'utilisateur l'arrête par erreur. On n'oubliera pas de donner la propriété de l'exécutable mon_appli à un utilisateur et un groupe non privilégiés.

    Le lecteur désireux d'expérimenter rapidement cette configuration pourra utiliser comme application en mode texte le Midnight Commander "mc" qui utilise la bibliothèque ncurses :

    # cp /usr/bin/mc /usr/local/bin/
    # chown nobody.nobody /usr/local/bin/mc
    

    Editer le fichier /etc/inittab, mettre en commentaire la ligne contenant le getty, et en ajouter une faisant démarrer le lanceur :

    # Run gettys in standard runlevels
    # 1:2345:respawn:/sbin/mingetty tty1
    1:2345:respawn:/usr/local/bin/lanceur_texte /dev/tty1
    /usr/local/bin/mc
    

    Demander à init de relire ce fichier.

    # kill -1 1
    

    Basculer sur le terminal virtuel 1 ( Contrôle-Alt-F1), et terminer le getty qui y fonctionne (avec Contrôle-D par exemple). A partir de ce moment, mc démarrera automatiquement sur cette console, avec une identité sans privilège (ce que l'on peut vérifier en essayant de déplacer des fichiers système).

    Application graphique sous X-Window

    Dans la majeure partie des cas actuels on désirera probablement faire tourner une application graphique sous XWindow plutôt qu'un logiciel en mode texte. Les problèmes qui se posent alors sont d'un autre ordre. Nous allons présenter ici une méthode pour les résoudre, mais d'autres techniques seraient sans doutes applicables, chacun pouvant essayer d'expérimenter à son gré.

    Le principe retenu ici est de regrouper toutes les commandes nécessaires dans un seul script shell que l'on lance directement dans /etc/inittab. On supprimera alors la ligne qui démarre Xdm, Kdm, Prefdm ou autres :

      x:45:respawn:/usr/X11R6/bin/xdm -nodaemon
    

    On remplacera cette ligne par :

      x:45:respawn:/usr/local/bin/graphique.sh
    

    Le script shell graphique.sh va donc faire démarrer un serveur X-Window en lançant le logiciel /usr/X11R6/bin/X. Dès lors l'écran sera initialisé en mode graphique, avec un fond contenant une sorte de damier - assez laid - et un curseur de souris en forme de croix. On peut observer cette configuration en invoquant la commande "xsetroot -def" dans une fenêtre Xterm sous X11.

    Pour pouvoir lancer notre application, nous devons reprendre la main, aussi le lancement de X doit-il être suivi d'un & pour le passer à l'arrière-plan. Nous pouvons dorénavant exécuter notre logiciel, en l'invoquant au travers du programme lanceur vu plus haut, qui limite les privilèges en modifiant les UID et GID. Il est obligatoire de faire suivre l'invocation du programme de l'argument " -display :0.0" qui sera analysé par la bibliothèque Xt, pour que l'application entre en contact avec le bon serveur X. Pour nos expériences, je propose de recopier le fichier exécutable xterm et de modifier son appartenance :

      # cp /usr/X11R6/bin/xterm /usr/local/bin/
      # chown nobody.nobody /usr/local/bin/xterm
    

    Notre premier script graphique.sh aura donc cette allure :

    #! /bin/sh
    /usr/X11R6/bin/X &
    /usr/local/bin/lanceur "/usr/local/bin/xterm -display :0.0"
    

    Cela fonctionne, mais l'ensemble est un peu décevant, le fond d'écran présente encore son allure de damier fatiguant pour les yeux, et le curseur de la souris conserve sa forme de croix X11. Nous voudrions donc modifier ces paramètres pour rendre l'aspect plus attrayant. Pour cela, il suffit d'invoquer l'utilitaire xsetroot, dont l'option -solid permet de préciser une couleur unie pour le fond, et l'option -cursor_name autorise le chargement d'un curseur de souris différent. Les noms des différents curseurs sont mentionnés dans le fichier /usr/include/X11/cursorfont.h (avec un préfixe XC_ à supprimer). Le curseur le plus courant est left_ptr qui affiche une petite flèche dirigée vers le haut à gauche.

    Nous essayons donc d'invoquer xsetroot avant notre application, et... cela ne fonctionne pas ! En effet, dès que la connexion entre le serveur X et son premier client (xsetroot) est rompue, le serveur se termine. Il faut donc que notre application démarre en premier, à l'arrière-plan, et que nous invoquions xsetroot par la suite. On peut imaginer quelque chose comme :

    #! /bin/sh
    /usr/X11R6/bin/X &
    /usr/local/bin/lanceur "/usr/local/bin/xterm -display :0.0" &
    /usr/X11R6/bin/xsetroot -solid black -cursor_name left_ptr -display :0.0
    

    Le problème qui apparaît à présent est que ce script se termine presque tout de suite puisque X et xterm sont à l'arrière-plan et que xsetroot dure très peu de temps. Comme nous avons configuré notre fichier /etc/inittab avec une commande respawn, init essaye de relancer le script immédiatement, et finit par boucler.

    La solution est de faire appel aux possibilités offertes par Bash pour passer un processus à l'arrière-plan, mémoriser son PID, et attendre la fin d'un processus particulier. Le script final va donc avoir l'aspect suivant :

    #!/bin/sh
    # Lancer X à l'arrière-plan
    /usr/X11R6/bin/X &
    # Mémoriser le PID de la dernière commande enarrière-plan (X)
    PID_X=$!
    
    # Lancer l'application à l'arrière-plan
    /usr/local/bin/lanceur "/usr/local/bin/xterm -geometry +200+200 -display :0.0 " &
    # Mémoriser son PID
    PID_APPLI=$!
    
    # Configurer le fond d'écran et le curseur avec xsetroot
    /usr/X11R6/bin/xsetroot -solid black - cursor_name left_ptr -display :0.0
    
    # Attendre la fin de l'application
    wait $PID_APPLI
    
    # Tuer par précaution le serveur X (qui devrait déjà être fini)
    kill -TERM $PID_X
    

    Cette fois-ci le résultat est exactement ce que l'on attendait. Le fond d'écran est acceptable, et l'application est relancée - ainsi que le serveur X - si jamais elle se termine. Le fait qu'aucun gestionnaire de fenêtre ne soit lancé prive l'application des bordures, barre de titre, etc. mais cela n'est pas contraignant si le poste est dédié à une seule utilisation donc a fortiori à une seule fenêtre principale.

    Conclusion

    Nous avons étudié trois manières de mettre en place une application pour qu'elle soit automatiquement lancée au démarrage de la machine, sans nécessiter d'intervention de l'utilisateur. La configuration d'une application sans interface utilisateur se fait de manière assez classique, en employant les techniques usuelles des démons Unix.

    En revanche la mise en service d'applications s'appuyant sur des interfaces en mode texte ou graphique est plus complexe, mais le résultat est intéressant car il montre bien la souplesse de paramétrage d'une machine Unix. J'ai utilisé avec succès ces configurations dans des logiciels de supervision de communication série (en mode texte), et des systèmes de visualisation graphique destinés à du personnel peu familiarisé avec Unix et X-Window (pompiers).

    Christophe Blaess ccb@club-internet.fr
    Linux Magazine France n°24 - Janvier 2001