Follow me
RSS feed
My sources
My Viadeo

Créer un démon (ou un service) pour U*IX et Windows en Java avec commons-deamon

Greg | 11 Apr 2011

Projets Créer un démon n'est pas des plus intuitif avec Java. Et bien qu'il y ait de l'espoir avec le futur Java 7, en attendant, il existe des solutions très simples à mettre en place. Je vous propose aujourd'hui de regarder ce que propose commons-daemon. Comme nous allons le voir, cette solution possède de solides avantages. Outre sa simplicité, elle permet, à moindre coût, de considérer les différentes plateformes.

Si nous avons l'habitude de parler de démon sous U*IX, un utilisateur Windows entendra plutôt service. Il s'agit d'une différence importante en terme de mise en place. En effet, si sous U*IX l'utilisation d'un fork suffit dans 90% des cas, sous Windows il faudra jouer avec le Service Control Manager. C'est justement ce que propose commons-daemon.

J'utiliserai indifféremment le terme de démon ou service dans la suite de cette présentation.

La partie commune

Dans cet exemple, j'utilise maven. Commencez donc par créer un projet et ajoutez la dépendance suivante :

<dependency>
   <groupId>commons-daemon</groupId>
   <artifactId>commons-daemon</artifactId>
   <version>1.0.3</version>
</dependency>

Nous allons maintenant créer une classe Runner contenant le corps de notre service. Pour cela je vous propose d'afficher à intervalle régulier un message :

Runner.java
 1 package net.algorithmique.sample;
 2 
 3 public class Runner {
 4    private static boolean started = false;
 5 
 6    public void Runner() {
 7    }
 8 
 9    public void start() {
10       started = true;
11 
12       System.out.println("My Service Started " + new java.util.Date());
13 
14       while (started) {
15          System.out.println("My Service is running " + new java.util.Date());
16          synchronized (this) {
17             try {
18                this.wait(10000);
19             } catch (InterruptedException ie) {
20             }
21          }
22       }
23 
24       System.out.println("My Service Finished " + new java.util.Date());
25    }
26 
27    public void stop() {
28       started = false;
29       synchronized (this) {
30          this.notify();
31       }
32    }
33 }

Il n'y a, dans ce code, rien de bien intéressant. En gros, nous nous contentons d'afficher un message toutes les 10 secondes.

C'est maintenant que nous allons rentrer dans la partie amusante.

commons-daemon est constitué de deux parties. Une première en Java qui met à notre disposition l'API Daemon. La seconde, en C, permet de gérer le service au niveau système.

Comme vous allez le voir, nous n'utiliserons l'API Daemon que pour les plateformes U*IX. Sous Windows, nous nous en passerons. De plus, la partie C sous Windows est différente de celle pour U*IX. Sous cette dernière plateforme, il s'agit de Jsvc alors que sous Windows il s'agit de Procrun. Cette différence vient principalement du fait que, sous Windows, commons-daemon permet de créer des services (au sens service Windows). Notion qui n'existe pas sur U*IX pour lequel nous avons tendance à parler de démon (au sens d'un processus forké). Si tout cela peut être un peu déroutant au départ, cela me semble être plutôt une bonne idée qui permet de conserver la spécificité de chaque plateforme.

Pour les U*IX

Comme je l'ai indiqué, nous allons utiliser ici l'API Daemon. Pour cela il suffit simplement de créer une classe implémentant Daemon. Cette implémentation nous impose de créer 4 méthodes dans notre classes :

Voici donc à quoi ressemblera notre classe UnixService :

UnixService.java
 1 package net.algorithmique.sample.unix;
 2 
 3 import net.algorithmique.sample.Runner;
 4 import org.apache.commons.daemon.Daemon;
 5 import org.apache.commons.daemon.DaemonContext;
 6 
 7 public class UnixService implements Daemon {
 8    private Runner runner;
 9 
10    public void init(DaemonContext daemonContext) throws Exception {
11       runner = new Runner();
12    }
13 
14    public void start() throws Exception {
15       runner.start();
16    }
17 
18    public void stop() throws Exception {
19       runner.stop();
20    }
21 
22    public void destroy() {
23    }
24 }

Nous sommes maintenant en mesure de démarrer notre service. Comme je l'ai dit plus haut, nous utilisons pour cela Jsvc. Il faudrait avant tout le récupérer. Pour cela, téléchargez la version correspondant à votre système. Vous avez le choix entre Linux, MacOSX ou Solaris. Si vous utilisez un autre système, vous devrez compiler vous même l'outil à partir des sources.

Une fois l'archive de Jsvc récupéré, décompressez là dans le répertoire de votre choix.

Jsvc s'utilise en lui passant en paramètres les informations nécessaires au lancement de notre démon. Les informations minimums à lui donner sont le classpath et le nom de la classe main. Dans notre cas, le classpath est assez simple puisqu'il ne comprend que le jar de commons-daemon et celui de notre démon. Pour ce dernier, j'ai simplement fait un mvn package. Nous pouvons donc lancer le démon avec la commande :

jsvc -cp /.../commons-daemon-1.0.3.jar:/.../sample-1.0-SNAPSHOT.jar net.algorithmique.sample.unix.UnixService

Dans les faits, utilisant maven sous Mac, commons-daemon-1.0.2.jar se trouve dans le répertoire ~/.m2/repository/commons-daemon/commons-daemon/1.0.3/ et le jar de mon projet se trouve dans target/

Pour arrêter le service, il suffit de réutiliser la même commande jsvc en lui ajoutant l'option -stop.

Tout ceci est bien joli, mais par forcement facile à utiliser. C'est pourquoi je me suis amusé à placer tout cela dans un script shell me permettant d'avoir accès aux classiques options start, stop, status et restart :

service.sh
 1 #!/bin/bash
 2 
 3 # DO NOT CHANGE THIS ! OR YOU REALLY KNOW WHAT YOU ARE DOING ;)
 4 export EXEC_PATH=`dirname $0`
 5 
 6 # Change this by the name of your daemon
 7 export DAEMON_NAME="Daemon"
 8 
 9 # Change this to match your classpath
10 export DAEMON_CLASSPATH=~/.m2/repository/commons-daemon/commons-daemon/1.0.3/commons-daemon-1.0.3.jar:$EXEC_PATH/../target/sample-1.0-SNAPSHOT.jar
11 
12 # Change this to specify the PID file path and name
13 export PID_FILE=$EXEC_PATH/service.pid
14 
15 # Change this to match you Daemon class
16 export MAIN_DAEMON_CLASS=net.algorithmique.sample.unix.UnixService
17 
18 # Change this to specify the stdout file
19 export STDOUT_FILE=$EXEC_PATH/../logs/stdout.txt
20 
21 # Change this to specify the stderr file
22 export STDERR_FILE=$EXEC_PATH/../logs/stderr.txt
23 
24 # Add -debug if you want to run in debug mode
25 export JSVC_OPTIONS=
26 
27 # -----------------------------------------------------------------------------
28 
29 export OS_TYPE=`uname`
30 if [ "x$OS_TYPE" == "xDarwin" ]; then
31    export EXEC="arch -arch i386 "$EXEC_PATH"/darwin/jsvc"
32 else
33    export EXEC=$EXEC_PATH"/linux/jsvc"
34 fi
35 
36 running() {
37    if [ -f $PID_FILE ]; then
38       echo $DAEMON_NAME" already running."
39       exit 0
40    fi
41 }
42 
43 start() {
44    running
45    $EXEC \
46       -cp $DAEMON_CLASSPATH \
47       -outfile $STDOUT_FILE \
48       -errfile $STDERR_FILE \
49       -pidfile $PID_FILE \
50       $JSVC_OPTIONS \
51       $MAIN_DAEMON_CLASS
52 }
53 
54 stop() {
55    $EXEC \
56       -cp $DAEMON_CLASSPATH \
57       -outfile $STDOUT_FILE \
58       -errfile $STDERR_FILE \
59       -pidfile $PID_FILE \
60       $JSVC_OPTIONS \
61       -stop \
62       $MAIN_DAEMON_CLASS
63 }
64 
65 case "$1" in
66    'start')
67       echo "Starting "$DAEMON_NAME"..."
68       start
69       ;;
70    'stop')
71       echo "Stopping "$DAEMON_NAME"..."
72       stop
73       ;;
74    'status')
75       if [ -f $PID_FILE ]; then
76          PID=`cat $PID_FILE`
77          echo $DAEMON_NAME" is running PID: "$PID
78       else
79          echo $DAEMON_NAME" is not running!"
80       fi
81       ;;
82    'restart')
83       $0 stop
84       sleep 5
85       $0 start
86       ;;
87    *)
88       echo $0 "start|stop|status|restart"
89       exit 1
90       ;;
91 esac
92 exit 0

Ce script est placé dans le sous répertoire bin de mon projet maven, dans lequel j'ai également un sous répertoire darwin pour la version Mac de Jsvc et un répertoire linux pour la version Linux.

Je vous laisse faire les modifications nécessaires pour les besoins d'une distribution.

Notez cependant une petite chose. Sous Mac, j'ai précédé l'appel de jsvc par arch -arch i386 pour forcer une utilisation en mise 32bits. La seule raison à cela vient du fait que si je laisse l'architecture par défaut (x86_64), Java me hurle dessus :

Cannot dynamically link to /System/Library/Frameworks/JavaVM.framework/Home/../Libraries/libclient.dylib
dlopen(/System/Library/Frameworks/JavaVM.framework/Home/../Libraries/libclient.dylib, 10): no suitable image found.  Did find:
	/System/Library/Frameworks/JavaVM.framework/Home/../Libraries/libclient.dylib: mach-o, but wrong architecture
java_init failed

Je n'ai pas cherché plus avant à résoudre ce problème. Si je trouve, ou si vous trouvez, un petit message dans les commentaires suffira à donner une réponse ;)

Pour Windows

Comme je l'ai signalé, ici nous n'avons pas besoin de l'API Daemon. Nous allons nous contenter de créer une classe contenant deux méthodes :

WinService.java
 1 package net.algorithmique.sample.windows;
 2 
 3 import net.algorithmique.sample.Runner;
 4 
 5 public class WinService {
 6    private static Runner runner = new Runner();
 7 
 8    static void start(String args[]) {
 9       runner.start();
10    }
11 
12    static void stop(String args[]) {
13       runner.stop();
14    }
15 }

Pour le reste, c'est l'outil prunsrv qui va gérer la mise en place du service.

Commencez donc par récupérer l'outil et décompressez-le dans le répertoire de votre choix.

prunsrv permet d'ajouter un service dans le Service Control Manager de Windows, de l'enlever, de le démarrer ou de le stopper. Pour cela, nous avons à notre disposition, les options suivantes :

En plus de ces informations, il faudra spécifier les options suivantes :

Nous pouvons donc installer le service via la commande :

prunsrv.exe //IS/MyService --Classpath=C:\...\sample-1.0-SNAPSHOT.jar --Description="Mon Service Java" --Jvm=auto --SartClass=net.algorithmique.sample.windows.WinService --StartMethod=start --StartMode=jvm --StopClass=net.algorithmique.sample.windows.WinService --StopMethod=stop --StopMode=jvm

Nous pouvons ensuite démarrer le service en remplaçant le //IS par //RS. Notez que par défaut le service est installé en démarrage manuel. Ceci peut être changé en ajoutant l'option --Startup=auto lors de l'installation du service.

Tout comme pour la version U*IX, j'ai créer un script permettant de faciliter l'installation, le démarrage, l'arrêt et la suppression du service :

service.bat
 1 @echo off
 2 
 3 rem -- DO NOT CHANGE THIS ! OR YOU REALLY KNOW WHAT YOU ARE DOING ;)
 4 set EXEC_PATH=%~dp0
 5 
 6 rem -- Service description
 7 set SERVICE_DESCRIPTION="My Java Service"
 8 
 9 rem -- Service name
10 set SERVICE_NAME=MyService
11 
12 rem -- Service CLASSPATH
13 set SERVICE_CLASSPATH=%EXEC_PATH%..\target\sample-1.0-SNAPSHOT.jar
14 
15 rem -- Service main class
16 set MAIN_SERVICE_CLASS=net.algorithmique.sample.windows.WinService
17 
18 rem -- Path for log files
19 set LOG_PATH=%EXEC_PATH%\..\logs
20 
21 rem -- STDERR log file
22 set ERR_LOG_FILE=%LOG_PATH%\stderr.txt
23 
24 rem -- STDOUT log file
25 set OUT_LOG_FILE=%LOG_PATH%\stdout.txt
26 
27 rem -- Startup mode (manual or auto)
28 set SERVICE_STARTUP=auto
29 
30 rem ---------------------------------------------------------------------------
31 set SERVICE_OPTIONS=--Description=%SERVICE_DESCRIPTION% --Jvm=auto --Classpath=%SERVICE_CLASSPATH% --StartMode=jvm --StartClass=%MAIN_SERVICE_CLASS% --StartMethod=start --StopMode=jvm --StopClass=%MAIN_SERVICE_CLASS% --StopMethod=stop --LogPath=%LOG_PATH% --StdOutput=%OUT_LOG_FILE% --StdError=%ERR_LOG_FILE% --Startup=%SERVICE_STARTUP%
32 
33 set RESTART=0
34 
35 :GETOPTS
36 if /I "%1" == "start" ( goto START )
37 if /I "%1" == "stop" ( goto STOP )
38 if /I "%1" == "console" ( goto CONSOLE )
39 if /I "%1" == "restart" ( goto RESTART )
40 if /I "%1" == "install" ( goto INSTALL )
41 if /I "%1" == "remove" ( goto REMOVE )
42 
43 goto HELP
44 
45 rem -- START ------------------------------------------------------------------
46 :START
47 
48 echo Start service %SERVICE_NAME%
49 %EXEC_PATH%windows\prunsrv.exe //RS/%SERVICE_NAME% %SERVICE_OPTIONS%
50 
51 goto FIN
52 
53 rem -- INSTALL ----------------------------------------------------------------
54 :INSTALL
55 
56 echo Install service %SERVICE_NAME%
57 %EXEC_PATH%windows\prunsrv.exe //IS/%SERVICE_NAME% %SERVICE_OPTIONS%
58 
59 goto FIN
60 
61 rem -- STOP -------------------------------------------------------------------
62 :STOP
63 
64 echo Stop service %SERVICE_NAME%
65 %EXEC_PATH%windows\prunsrv.exe //SS/%SERVICE_NAME% %SERVICE_OPTIONS%
66 
67 if "%RESTART%" == "1" ( goto START )
68 goto FIN
69 
70 rem -- REMOVE -----------------------------------------------------------------
71 :REMOVE
72 
73 echo Remove service %SERVICE_NAME%
74 %EXEC_PATH%windows\prunsrv.exe //DS/%SERVICE_NAME% %SERVICE_OPTIONS%
75 
76 goto FIN
77 
78 rem -- CONSOLE ----------------------------------------------------------------
79 :CONSOLE
80 
81 %EXEC_PATH%windows\prunsrv.exe //TS/%SERVICE_NAME% %SERVICE_OPTIONS%
82 
83 goto FIN
84 
85 rem -- RESTART ----------------------------------------------------------------
86 :RESTART
87 
88 set RESTART=1
89 
90 goto STOP
91 
92 rem -- HELP -------------------------------------------------------------------
93 :HELP
94 
95 echo "service.bat install|remove|start|stop|restart"
96 goto FIN
97 
98 :FIN

L'exemple présenté ici est relativement simple, mais, j'espère, suffisant pour vous permettre de mettre en place facilement des services à destination de vos utilisateurs, quelle que soit leur machine. Comme toujours, je vous conseille d'aller jeter un oeil sur la documentation officielle.

Copyright © 2009 - 2011 Grégoire Lejeune.
All documents licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 2.5 License, except ones with specified licence.
Powered by Jekyll.