I. Introduction▲
Comment empêcher de lancer plusieurs fois simultanément une application Java ?
Par exemple, vous venez de programmer un serveur, vous voulez vous assurer qu'une seule instance est lancée, pour éviter de provoquer des comportements inattendus (une instance reçoit une requête d'un client, et l'autre reçoit son identifiant). En général, quel que soit le programme, il peut être pratique d'interdire plusieurs lancements de la même application.
C'est à cette question que cet article se propose de répondre.
Pour ceux qui se fichent du fonctionnement, mais qui veulent pouvoir utiliser la solution mise en œuvre, allez directement prendre le code source à la fin de ce tutoriel.
Il ne faut pas confondre instance unique d'une application et instance unique d'une classe, cela n'a rien à voir. Une instance unique de classe peut être assurée en utilisant le design pattern Singleton.
public
final
class
MonSingleton {
private
static
final
MonSingleton instance =
new
MonSingleton
(
);
private
MonSingleton
(
) {
//...
}
public
static
MonSingleton getMonSingleton
(
) {
return
instance;
}
}
II. Solutions▲
Reformulons le problème : pour empêcher plusieurs instances d'être lancées simultanément, il suffit, lors du démarrage, de vérifier qu'aucune autre n’est déjà lancée. Le problème est alors « comment vérifier qu'aucune autre n'est déjà lancée ? » Pour cela, il leur faut un moyen de communication.
Nous n'aborderons pas ici d'éventuelles solutions qui ne seraient pas multiplateformes : notre solution doit fonctionner aussi bien sous Windows que sous Linux et Mac.
II-A. Fichiers▲
II-A-1. Fichier existant▲
La première solution qui vient à l'esprit, c'est de laisser une empreinte (par exemple un fichier) lorsque l'application démarre, et de la supprimer lorsque l'application se termine. Cette solution semble intéressante, mais elle pose un problème : peut-on supprimer l'empreinte, quelle que soit la manière dont l'application est quittée ? Si l'utilisateur ferme la fenêtre, pas de problèmes a priori, il suffit de lancer une méthode qui supprime l'empreinte. Pourtant, sous MacOS, cela pose un problème : selon la manière dont l'utilisateur ferme la fenêtre, soit cela fonctionne correctement, soit l'application est tout simplement tuée. Et lorsque l'application est tuée (quel que soit le système d'exploitation), eh bien on n'exécute pas du code grâce à un écouteur de fenêtre… Vous allez me dire, et vous aurez raison, on n'a qu'à ajouter un hook (voir ici) pour exécuter du code lorsque l'application est tuée (à condition que ce code soit rapide à exécuter). Mais bon, l'argument qui balaye tout, c'est « et s'il y a une panne de courant ? », « Et si l'ordinateur plante lamentablement (blue screen sous Windows) ? » Là, vous pouvez mettre un hook ou tout ce que vous voulez, aucun code ne sera exécuté. Vous me direz, ça n'arrive pas si souvent… Mais lorsque ça arrive, il ne vous sera plus jamais possible de lancer l'application (une autre est toujours « lancée »). Si c'est pour vous, ça va, vous savez qu'il suffit de supprimer le fichier. Si c'est pour distribuer à des clients…
II-A-2. Fichier verrouillé▲
La solution en utilisant l'existence d'un fichier n'est donc pas sûre. Cependant, on peut utiliser un fichier différemment pour résoudre notre problème : poser un verrou dessus. Cette solution fonctionne très bien, elle a l'avantage d'être extrêmement simple. Même si ça n'est pas la méthode retenue dans cet article, elle peut très bien répondre à vos besoins. Voici comment tester l'unicité de l'application en utilisant cette solution :
public
boolean
isUnique
(
) {
boolean
unique;
try
{
unique =
new
FileOutputStream
(
"lock"
).getChannel
(
).tryLock
(
) !=
null
;
}
catch
(
IOException ie) {
unique =
false
;
}
return
unique;
}
II-B. Socket▲
Si c'est la première fois que vous entendez parler de cette solution, vous allez sans doute bondir de votre siège (j'exagère), en pensant « une socket, pour faire ça !?! ». Et pourtant… Cette méthode est, d'après moi, la meilleure. Pourquoi ? D'abord, il n'y a pas 36 façons pour faire communiquer deux programmes totalement dissociés tout en gardant la portabilité : a priori, les deux méthodes décrites dans cet article. Ensuite, en utilisant des sockets, on peut effectuer une vraie communication. Par exemple, lorsqu'une deuxième instance de l'application essaye de démarrer, elle peut communiquer avec la première, pour lui demander de passer au premier plan, ce que ne permet pas de faire la solution utilisant un verrou sur un fichier. Par contre, cette méthode a bien sûr un inconvénient, elle utilise un port de communication, et si ce port est utilisé par une autre application, cela ne fonctionnera pas. Mais, si vous prenez au hasard un port entre 10000 et 65535, il y a très peu de chances qu'il soit déjà utilisé (et au pire, on peut implanter une configuration de ce port grâce à un fichier de configuration).
III. Mise en œuvre▲
Nous allons donc mettre en œuvre la méthode basée sur des sockets.
III-A. Besoins▲
De quoi a-t-on besoin pour cela ? Pour répondre à cette question, voyons ce que nous désirons obtenir. Nous voudrions pouvoir utiliser le gestionnaire d'instance unique très simplement, par exemple, dans un premier temps :
// On crée un gestionnaire d'instance unique en précisant le port à utiliser.
UniqueInstance uniqueInstance =
new
UniqueInstance
(
PORT);
if
(
uniqueInstance.uneMethodeIndiquantSiAucuneAutreInstanceEstDejaLancee
(
)) {
// Lancer l'application
application =
new
MonApplication
(
);
}
On a vu qu'avec des sockets, il était possible de signaler à l'instance déjà lancée qu'une autre avait essayé de démarrer. Donc on aimerait pouvoir passer en paramètres le message de communication (message à envoyer par l'application qui essaye de démarrer vers l'application déjà lancée) et les actions à effectuer en cas de réception de ce message.
Runnable actions =
new
Runnable
(
) {
public
void
run
(
) {
// Mettre la fenêtre au premier plan
}
}
// On crée un gestionnaire d'instance unique en précisant le port à utiliser, le message
// de communication (par exemple le nom de l'application) et les actions à effectuer.
UniqueInstance uniqueInstance =
new
UniqueInstance
(
PORT, "monApplication"
, actions);
if
(
uniqueInstance.uneMethodeIndiquantSiAucuneAutreInstanceEstDejaLancee
(
)) {
// Lancer l'application
application =
new
MonApplication
(
);
}
III-B. Code source▲
Maintenant que l'on a vu ce que l'on désirait obtenir, je vous donne l'implantation que j'ai faite de la méthode basée sur des sockets, bien sûr totalement libre de droits. L'astuce principale de la méthode est que l'ouverture d'un ServerSocket sur un port déjà utilisé lève une exception.
import
java.io.IOException;
import
java.io.PrintWriter;
import
java.net.ServerSocket;
import
java.net.Socket;
import
java.util.Scanner;
import
java.util.logging.Logger;
/**
* Cette classe permet d'assurer l'unicité de l'instance de l'application. Deux applications ne peuvent pas être lancées
* simultanément. Voici un exemple typique d'utilisation :
*
*
<
pre
>
* // Port à utiliser pour communiquer avec l'instance de l'application lancée.
* final int PORT = 32145;
* // Message à envoyer à l'application lancée lorsqu'une autre instance essaye de démarrer.
* final String MESSAGE = "nomDeMonApplication";
* // Actions à effectuer lorsqu'une autre instance essaye de démarrer.
* final Runnable RUN_ON_RECEIVE = new Runnable() {
* public void run() {
* if(mainFrame != null) {
* // Si la fenêtre n'est pas visible (uniquement dans le systray par exemple), on la rend visible.
* if(!mainFrame.isVisible())
* mainFrame.setVisible(true);
* // On demande à la mettre au premier plan.
* mainFrame.toFront();
* }
* }
* });
*
* UniqueInstance uniqueInstance = new UniqueInstance(PORT, MESSAGE, RUN_ON_RECEIVE);
* // Si aucune autre instance n'est lancée...SS
* if(uniqueInstance.launch()) {
* // On démarre l'application.
* new MonApplication();
* }
*
<
/pre
>
*
*
@author
rom1v
*/
public
class
UniqueInstance {
/** Port d'écoute utilisé pour l'unique instance de l'application. */
private
int
port;
/** Message à envoyer à l'éventuelle application déjà lancée. */
private
String message;
/** Actions à effectuer lorsqu'une autre instance de l'application a indiqué qu'elle avait essayé de démarrer. */
private
Runnable runOnReceive;
/**
* Créer un gestionnaire d'instance unique de l'application.
*
*
@param
port
* Port d'écoute utilisé pour l'unique instance de l'application.
*
@param
message
* Message à envoyer à l'éventuelle application déjà lancée,
{@code
null
}
si aucune action.
*
@param
runOnReceive
* Actions à effectuer lorsqu'une autre instance de l'application a indiqué qu'elle avait essayé de
* démarrer,
{@code
null
}
pour aucune action.
*
@param
runOnReceive
* Actions à effectuer lorsqu'une autre instance de l'application a indiqué qu'elle
* avait essayé de démarrer,
{@code
null
}
pour aucune action.
*
@throws
IllegalArgumentException
* si le port n'est pas compris entre 1 et 65535, ou si
*
{@code
runOnReceive != null && message == null
}
(s'il y a des actions à
* effectuer, le message ne doit pas être
{@code
null
}
.
*/
public
UniqueInstance
(
int
port, String message, Runnable runOnReceive) {
if
(
port ==
0
||
(
port &
0xffff0000
) !=
0
)
throw
new
IllegalArgumentException
(
"Le port doit être compris entre 1 et 65535 : "
+
port +
"."
);
if
(
runOnReceive !=
null
&&
message ==
null
)
throw
new
IllegalArgumentException
(
"runOnReceive != null ==> message == null."
);
this
.port =
port;
this
.message =
message;
this
.runOnReceive =
runOnReceive;
}
/**
* Créer un gestionnaire d'instance unique de l'application. Ce constructeur désactive la communication entre
* l'instance déjà lancée et l'instance qui essaye de démarrer.
*
*
@param
port
* Port d'écoute utilisé pour l'unique instance de l'application.
*/
public
UniqueInstance
(
int
port) {
this
(
port, null
, null
);
}
/**
* Essaye de démarrer le gestionnaire d'instance unique. Si l'initialisation a réussi, c'est que l'instance est
* unique. Sinon, c'est qu'une autre instance de l'application est déjà lancée. L'appel de cette méthode prévient
* l'application déjà lancée qu'une autre vient d'essayer de se connecter.
*
*
@return
{@code
true
}
si l'instance de l'application est unique.
*/
public
boolean
launch
(
) {
/* Indique si l'instance du programme est unique. */
boolean
unique;
try
{
/* On crée une socket sur le port défini. */
final
ServerSocket server =
new
ServerSocket
(
port);
/* Si la création de la socket réussit, c'est que l'instance du programme est unique, aucune autre n'existe. */
unique =
true
;
/* S'il y a des actions à faire lorsqu'une autre instance essaye de démarrer... */
if
(
runOnReceive !=
null
) {
/* On lance un Thread d'écoute sur ce port. */
Thread portListenerThread =
new
Thread
(
"UniqueInstance-PortListenerThread"
) {
{
setDaemon
(
true
);
}
@Override
public
void
run
(
) {
/* Tant que l'application est lancée... */
while
(
true
) {
try
{
/* On attend qu'une socket se connecte sur le serveur. */
final
Socket socket =
server.accept
(
);
/* Si une socket est connectée, on écoute le message envoyé dans un nouveau Thread. */
new
Thread
(
"UniqueInstance-SocketReceiver"
) {
{
setDaemon
(
true
);
}
@Override
public
void
run
(
) {
receive
(
socket);
}
}
.start
(
);
}
catch
(
IOException e) {
Logger.getLogger
(
"UniqueInstance"
).warning
(
"Attente de connexion de socket échouée."
);
}
}
}
}
;
/* On démarre le Thread. */
portListenerThread.start
(
);
}
}
catch
(
IOException e) {
/* Si la création de la socket échoue, c'est que l'instance n'est pas unique, une autre n'existe. */
unique =
false
;
/* Si des actions sont prévues par l'instance déjà lancée... */
if
(
runOnReceive !=
null
) {
/*
* Dans ce cas, on envoie un message à l'autre instance de l'application pour lui demander d'avoir le
* focus (par exemple).
*/
send
(
);
}
}
return
unique;
}
/**
* Envoie un message à l'instance de l'application déjà ouverte.
*/
private
void
send
(
) {
PrintWriter pw =
null
;
try
{
/* On se connecte sur la machine locale. */
Socket socket =
new
Socket
(
"localhost"
, port);
/* On définit un PrintWriter pour écrire sur la sortie de la socket. */
pw =
new
PrintWriter
(
socket.getOutputStream
(
));
/* On écrit le message sur la socket. */
pw.write
(
message);
}
catch
(
IOException e) {
Logger.getLogger
(
"UniqueInstance"
).warning
(
"Écriture sur flux de sortie de la socket échouée."
);
}
finally
{
if
(
pw !=
null
)
pw.close
(
);
}
}
/**
* Reçoit un message d'une socket s'étant connectée au serveur d'écoute. Si ce message est le message de l'instance
* unique, l'application demande le focus.
*
*
@param
socket
* Socket connectée au serveur d'écoute.
*/
private
synchronized
void
receive
(
Socket socket) {
Scanner sc =
null
;
try
{
/* On n'écoute que 5 secondes, si aucun message n'est reçu, tant pis... */
socket.setSoTimeout
(
5000
);
/* On définit un Scanner pour lire sur l'entrée de la socket. */
sc =
new
Scanner
(
socket.getInputStream
(
));
/* On ne lit qu'une ligne. */
String s =
sc.nextLine
(
);
/* Si cette ligne est le message de l'instance unique... */
if
(
message.equals
(
s)) {
/* On exécute le code demandé. */
runOnReceive.run
(
);
}
}
catch
(
IOException e) {
Logger.getLogger
(
"UniqueInstance"
).warning
(
"Lecture du flux d'entrée de la socket échoué."
);
}
finally
{
if
(
sc !=
null
)
sc.close
(
);
}
}
}
III-C. Exemple d'utilisation▲
Voici un exemple typique d'utilisation (celui présent dans la javadoc) :
// Port à utiliser pour communiquer avec l'instance de l'application lancée.
final
int
PORT =
32145
;
// Message à envoyer à l'application lancée lorsqu'une autre instance essaye de démarrer.
final
String MESSAGE =
"nomDeMonApplication"
;
// Actions à effectuer lorsqu'une autre instance essaye de démarrer.
final
Runnable RUN_ON_RECEIVE =
new
Runnable
(
) {
public
void
run
(
) {
// On exécute ce runnable dans l'EDT
SwingUtilities.invokeLater
(
new
Runnable
(
) {
public
void
run
(
) {
if
(
mainFrame !=
null
) {
// Si la fenêtre n'est pas visible (uniquement dans le systray par exemple), on la rend visible.
if
(!
mainFrame.isVisible
(
))
mainFrame.setVisible
(
true
);
// On demande à la mettre au premier plan.
mainFrame.toFront
(
);
}
}
}
);
}
}
);
UniqueInstance uniqueInstance =
new
UniqueInstance
(
PORT, MESSAGE, RUN_ON_RECEIVE);
// Si aucune autre instance n'est lancée...
if
(
uniqueInstance.launch
(
)) {
// On démarre l'application.
new
MonApplication
(
);
}
Dans cet exemple, mainFrame est la fenêtre principale de l'application (l'accès à cette fenêtre peut être fait différemment selon la manière dont vous avez organisé votre application).