Instance unique d'application en java

Romain Vimont (®om)

- Titre : Instance unique d'application en java

- Auteur : Romain Vimont (®om)

- Parution : 1 janvier 2006

- Mise à jour : 8 novembre 2019

- Licence : Ce document est issu de http://www.developpez.com et reste la propriété exclusive de son auteur. La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.

Sommaire

- I. Introduction

- II. Solutions

- II-A. Fichiers

- II-A-1. Fichier existant

- II-A-2. Fichier verrouillé

- II-B. Socket

- III. Mise en œuvre

- III-A. Besoins

- III-B. Code source

- III-C. Exemple d'utilisation

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.

Ce code est aussi disponible en page html ici, et la javadoc est disponible ici.

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 = &quot;nomDeMonApplication&quot;;
* // 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).