Créer un démon (ou un service) pour U*IX et Windows en Java avec commons-deamon
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 :
- init : cette méthode est appelée lors de l'initialisation du service.
- start : c'est la méthode appelée au démarrage du service.
- stop : cette méthode est appelée quand nous arrêtons le service
- destroy : cette méthode est appelée, après l'arrêt du service, au moment de le libérer.
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 :
- start : appelée au démarrage du service.
- stop : appelée quand nous arrêtons le service.
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 :
- //IS/NomDuService : pour l'installation du service.
- //DS/NomDuService : pour la suppression du service.
- //RS/NomDuService : pour le démarrage du service.
- //SS/NomDuService : pour stopper le service.
En plus de ces informations, il faudra spécifier les options suivantes :
- --Description"Description du service" : description du service vu dans le Service Control Manager.
- --Classpath=CLASSPATH : le classpath.
- --StartClass=MainClass : nom de la classe main.
- --StartMethod=start : méthode de la classe main appelée pour le démarrage du service.
- --StopClass=MainClass : nom de la classe main.
- --StopMethod=stop : méthode de la classe main appelée pour l'arrêt du service.
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.