Comment implémenter une application Java à instance unique?

89

Parfois, je vois de nombreuses applications telles que msn, Windows Media Player, etc. qui sont des applications à instance unique (lorsque l'utilisateur s'exécute pendant que l'application exécute une nouvelle instance d'application ne sera pas créée).

En C #, j'utilise la Mutexclasse pour cela mais je ne sais pas comment faire cela en Java.

Fuang avec S.
la source
Une approche très simple avec java NIO voir l'exemple complet stackoverflow.com/a/20015771/185022
AZ_

Réponses:

62

Si je crois cet article , par:

avoir la première tentative d'ouvrir une socket d'écoute sur l'interface localhost. S'il est capable d'ouvrir le socket, on suppose qu'il s'agit de la première instance de l'application à être lancée. Sinon, l'hypothèse est qu'une instance de cette application est déjà en cours d'exécution. La nouvelle instance doit informer l'instance existante qu'une tentative de lancement a été effectuée, puis quitter. L'instance existante prend le relais après avoir reçu la notification et déclenche un événement vers l'écouteur qui gère l'action.

Remarque: Ahe mentionne dans le commentaire que l'utilisation InetAddress.getLocalHost()peut être délicate:

  • cela ne fonctionne pas comme prévu dans l'environnement DHCP car l'adresse renvoyée dépend du fait que l'ordinateur dispose ou non d'un accès réseau.
    La solution était d'ouvrir la connexion avec InetAddress.getByAddress(new byte[] {127, 0, 0, 1});
    Probablement lié au bogue 4435662 .
  • J'ai également trouvé le bogue 4665037 qui rapporte que Résultats attendus de getLocalHost: retour de l'adresse IP de la machine, vs résultats réels: retour 127.0.0.1.

il est surprenant d'avoir un getLocalHostretour 127.0.0.1sur Linux mais pas sur windows.


Ou vous pouvez utiliser un ManagementFactoryobjet. Comme expliqué ici :

La getMonitoredVMs(int processPid)méthode reçoit comme paramètre le PID de l'application en cours et récupère le nom de l'application qui est appelé à partir de la ligne de commande, par exemple, l'application a été démarrée à partir du c:\java\app\test.jarchemin, la variable de valeur est " c:\\java\\app\\test.jar". De cette façon, nous attraperons juste le nom de l'application sur la ligne 17 du code ci-dessous.
Après cela, nous recherchons dans JVM un autre processus avec le même nom, si nous l'avons trouvé et que le PID de l'application est différent, cela signifie qu'il s'agit de la deuxième instance d'application.

JNLP offre également un SingleInstanceListener

VonC
la source
3
Sachez que la première solution a un bogue. Nous avons récemment découvert que InetAddress.getLocalHost()cela ne fonctionne pas comme prévu dans l'environnement DHCP car l'adresse renvoyée dépend de l'accès réseau de l'ordinateur. La solution était d'ouvrir la connexion avec InetAddress.getByAddress(new byte[] {127, 0, 0, 1});.
Ahe le
2
@Ahe: excellent point. J'ai inclus votre commentaire ainsi que les références de rapport de bogue Oracle-Sun dans ma réponse modifiée.
VonC le
3
Selon JavaDoc InetAddress.getByName(null)renvoie l'adresse de l'interface de bouclage. Je suppose que c'est mieux que de spécifier manuellement 127.0.0.1 car en théorie cela devrait également fonctionner dans les environnements IPv6 uniquement.
kayahr
3
Voir également Utilisation du service SingleInstanceService .
trashgod
1
@Puce Bien sûr, pas de problème: j'ai restauré ces liens.
VonC le
65

J'utilise la méthode suivante dans la méthode principale. C'est la méthode la plus simple, la plus robuste et la moins intrusive que j'ai vue, alors j'ai pensé que je la partagerais.

private static boolean lockInstance(final String lockFile) {
    try {
        final File file = new File(lockFile);
        final RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
        final FileLock fileLock = randomAccessFile.getChannel().tryLock();
        if (fileLock != null) {
            Runtime.getRuntime().addShutdownHook(new Thread() {
                public void run() {
                    try {
                        fileLock.release();
                        randomAccessFile.close();
                        file.delete();
                    } catch (Exception e) {
                        log.error("Unable to remove lock file: " + lockFile, e);
                    }
                }
            });
            return true;
        }
    } catch (Exception e) {
        log.error("Unable to create and/or lock file: " + lockFile, e);
    }
    return false;
}
Robert
la source
quel devrait être le paramètre "lockFile" pour une application de bureau? le nom du fichier jar de l'application? que diriez-vous qu'il n'y a pas de fichier jar juste quelques fichiers de classe?
5YrsLaterDBA
2
Est-il vraiment nécessaire de libérer manuellement le verrou de fichier et de fermer le fichier à l'arrêt? Cela ne se produit-il pas automatiquement lorsque le processus meurt?
Natix
5
Mais que se passe-t-il si l'alimentation est coupée et que l'ordinateur s'arrête sans exécuter le crochet d'arrêt? Le fichier persistera et l'application ne pourra pas être lancée.
Petr Hudeček
6
@ PetrHudeček Tout va bien. Quelle que soit la façon dont l'application se termine, le verrou de fichier sera libéré. S'il ne s'agissait pas d'un arrêt correct, cela a même l'avantage de permettre à l'application de le réaliser lors de la prochaine exécution. Dans tous les cas: c'est le verrou qui compte, pas la présence du fichier lui-même. Si le fichier est toujours là, l'application se lancera quand même.
Dreamspace President
@Robert: Merci pour votre solution, je l'utilise depuis. Et tout à l'heure, je l'ai étendu pour communiquer également à l'instance déjà existante qu'une autre instance a essayé de démarrer - en utilisant le dossier WatchService! stackoverflow.com/a/36772436/3500521
Dreamspace President
9

Si l'application. a une interface graphique, lancez-la avec JWS et utilisez le SingleInstanceService.

Mettre à jour

Le plug-in Java (requis pour les applets et les applications JWS) a été abandonné par Oracle et supprimé du JDK. Les fabricants de navigateurs l'avaient déjà supprimé de leurs navigateurs.

Cette réponse est donc caduque. Ne le laissant ici que pour avertir les personnes qui consultent de la documentation ancienne.

Andrew Thompson
la source
2
Notez également qu'il semble que l'instance en cours d'exécution puisse être informée des nouvelles instances et de leurs arguments, ce qui facilite la communication dans un tel programme.
Thorbjørn Ravn Andersen
6

Oui, c'est une réponse vraiment décente pour l'application d'instance unique eclipse RCP eclipse ci-dessous est mon code

dans application.java

if(!isFileshipAlreadyRunning()){
        MessageDialog.openError(display.getActiveShell(), "Fileship already running", "Another instance of this application is already running.  Exiting.");
        return IApplication.EXIT_OK;
    } 


private static boolean isFileshipAlreadyRunning() {
    // socket concept is shown at http://www.rbgrn.net/content/43-java-single-application-instance
    // but this one is really great
    try {
        final File file = new File("FileshipReserved.txt");
        final RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
        final FileLock fileLock = randomAccessFile.getChannel().tryLock();
        if (fileLock != null) {
            Runtime.getRuntime().addShutdownHook(new Thread() {
                public void run() {
                    try {
                        fileLock.release();
                        randomAccessFile.close();
                        file.delete();
                    } catch (Exception e) {
                        //log.error("Unable to remove lock file: " + lockFile, e);
                    }
                }
            });
            return true;
        }
    } catch (Exception e) {
       // log.error("Unable to create and/or lock file: " + lockFile, e);
    }
    return false;
}
parvez Ahmad
la source
5

Nous utilisons le verrouillage de fichier pour cela (saisir un verrou exclusif sur un fichier magique dans le répertoire de données de l'application de l'utilisateur), mais nous sommes principalement intéressés à empêcher plusieurs instances de s'exécuter.

Si vous essayez de faire passer la deuxième instance des arguments de ligne de commande, etc ... à la première instance, alors utiliser une connexion socket sur localhost tuera deux oiseaux avec une pierre. Algorithme général:

  • Au lancement, essayez d'ouvrir l'écouteur sur le port XXXX sur localhost
  • en cas d'échec, ouvrez un graveur sur ce port sur l'hôte local et envoyez les arguments de ligne de commande, puis arrêtez
  • sinon, écoutez sur le port XXXXX sur localhost. Lorsque vous recevez des arguments de ligne de commande, traitez-les comme si l'application avait été lancée avec cette ligne de commande.
Kevin Day
la source
5

J'ai trouvé une solution, une explication un peu caricaturale, mais qui fonctionne toujours dans la plupart des cas. Il utilise l'ancien fichier de verrouillage pour créer des trucs, mais dans une vue assez différente:

http://javalandscape.blogspot.com/2008/07/single-instance-from-your-application.html

Je pense que ce sera une aide pour ceux qui ont un paramètre de pare-feu strict.

Icône
la source
Oui, c'est un bon moyen car le verrou serait libéré en cas de panne de l'application ou plus :)
LE GALL Benoît
5

Vous pouvez utiliser la bibliothèque JUnique. Il prend en charge l'exécution d'une application Java à instance unique et est open source.

http://www.sauronsoftware.it/projects/junique/

La bibliothèque JUnique peut être utilisée pour empêcher un utilisateur d'exécuter en même temps plusieurs instances de la même application Java.

JUnique implémente des verrous et des canaux de communication partagés entre toutes les instances JVM lancées par le même utilisateur.

public static void main(String[] args) {
    String appId = "myapplicationid";
    boolean alreadyRunning;
    try {
        JUnique.acquireLock(appId, new MessageHandler() {
            public String handle(String message) {
                // A brand new argument received! Handle it!
                return null;
            }
        });
        alreadyRunning = false;
    } catch (AlreadyLockedException e) {
        alreadyRunning = true;
    }
    if (!alreadyRunning) {
        // Start sequence here
    } else {
        for (int i = 0; i < args.length; i++) {
            JUnique.sendMessage(appId, args[0]));
        }
    }
}

Sous le capot, il crée des verrous de fichiers dans le dossier% USER_DATA% /. Junique et crée un socket serveur sur un port aléatoire pour chaque appId unique qui permet d'envoyer / recevoir des messages entre les applications java.

kolobok
la source
Puis-je l'utiliser pour empêcher plusieurs instances d'application Java dans un réseau? aka, une seule instance de mon application est autorisée sur tout mon réseau
Wuaner
4

Sous Windows, vous pouvez utiliser launch4j .

Jacek Szymański
la source
2

Classe ManagementFactory prise en charge dans le détail J2SE 5.0 ou version ultérieure

mais maintenant j'utilise J2SE 1.4 et j'ai trouvé celui-ci http://audiprimadhanty.wordpress.com/2008/06/30/ensuring-one-instance-of-application-running-at-one-time/ mais je n'ai jamais testé. Qu'est-ce que tu en penses?

Fuang avec S.
la source
Je crois que c'est essentiellement ce qui est décrit dans le premier lien de ma réponse ci-dessus ... rbgrn.net/blog/2008/05/java-single-application-instance.html
VonC
2

Vous pouvez essayer d'utiliser l'API Preferences. Il est indépendant de la plateforme.

Javamann
la source
J'aime cette idée car l'API est simple, mais peut-être que certains scanners de virus n'aimeraient pas que vous changiez le registre de sorte que vous rencontriez des problèmes similaires à ceux de l'utilisation de RMI sur des systèmes avec un pare-feu logiciel .... pas sûr.
Cal.1
@Cal Mais le même problème est avec le changement de fichier / le verrouillage / etc.
Alex
2

Une manière plus générique de limiter le nombre d'instances sur une seule machine, voire sur un réseau entier, consiste à utiliser un socket multicast.

L'utilisation d'un socket de multidiffusion vous permet de diffuser un message vers n'importe quel nombre d'instances de votre application, dont certaines peuvent se trouver sur des machines physiquement distantes sur un réseau d'entreprise.

De cette façon, vous pouvez activer de nombreux types de configurations, pour contrôler des éléments tels que

  • Une ou plusieurs instances par machine
  • Une ou plusieurs instances par réseau (par exemple, contrôle des installations sur un site client)

Le support multicast de Java se fait via le package java.net avec MulticastSocket et DatagramSocket étant les principaux outils.

Remarque : MulticastSocket ne garantit pas la livraison des paquets de données, vous devez donc utiliser un outil construit sur des sockets multicast comme JGroups . JGroups fait la livraison de garantie de toutes les données. Il s'agit d'un seul fichier jar, avec une API très simple.

JGroups existe depuis un certain temps et a des utilisations impressionnantes dans l'industrie, par exemple, il sous-tend le mécanisme de clustering de JBoss pour diffuser des données à toutes les instances d'un cluster.

Utiliser JGroups, limiter le nombre d'instances d'une application (sur une machine ou un réseau, disons: au nombre de licences qu'un client a achetées) est conceptuellement très simple:

  • Au démarrage de votre application, chaque instance essaie de rejoindre un groupe nommé, par exemple "My Great App Group". Vous aurez configuré ce groupe pour autoriser 0, 1 ou N membres
  • Lorsque le nombre de membres du groupe est supérieur à ce que vous avez configuré pour cela, votre application doit refuser de démarrer.
johnm
la source
1

Vous pouvez ouvrir un fichier mappé en mémoire et voir si ce fichier est déjà OUVERT. s'il est déjà ouvert, vous pouvez revenir de main.

Une autre façon est d'utiliser des fichiers de verrouillage (pratique standard Unix). Une autre façon est de mettre quelque chose dans le presse-papiers au démarrage de main après avoir vérifié si quelque chose se trouve déjà dans le presse-papiers.

Sinon, vous pouvez ouvrir une socket en mode écoute (ServerSocket). Essayez d'abord de vous connecter à la prise hte; si vous ne pouvez pas vous connecter, ouvrez un serveur de socket. si vous vous connectez, vous savez qu'une autre instance est déjà en cours d'exécution.

Ainsi, à peu près n'importe quelle ressource système peut être utilisée pour savoir qu'une application est en cours d'exécution.

BR, ~ A

Anjanb
la source
avez-vous du code pour l'une de ces idées? aussi, que faire si je veux que si l'utilisateur démarre une nouvelle instance, il fermera toutes les précédentes?
développeur android
1

J'ai utilisé des sockets pour cela et selon si l'application est côté client ou côté serveur, le comportement est un peu différent:

  • côté client: si une instance existe déjà (je ne peux pas écouter sur un port spécifique) je passerai les paramètres de l'application et quitterai (vous voudrez peut-être effectuer certaines actions dans l'instance précédente) sinon je vais démarrer l'application.
  • côté serveur: si une instance existe déjà, j'imprimerai un message et quitterai, sinon je lancerai l'application.
adrian.tarau
la source
1
classe publique SingleInstance {
    public static final String LOCK = System.getProperty ("user.home") + File.separator + "test.lock";
    public static final String PIPE = System.getProperty ("user.home") + File.separator + "test.pipe";
    Trame JFrame statique privée = null;

    public static void main (String [] args) {
        essayez {
            FileChannel lockChannel = nouveau RandomAccessFile (LOCK, "rw"). GetChannel ();
            FileLock flk = null; 
            essayez {
                flk = lockChannel.tryLock ();
            } catch (Throwable t) {
                t.printStackTrace ();
            }
            if (flk == null ||! flk.isValid ()) {
                System.out.println ("déjà en cours d'exécution, laisser un message à diriger et quitter ...");
                FileChannel pipeChannel = null;
                essayez {
                    pipeChannel = new RandomAccessFile (PIPE, "rw"). getChannel ();
                    MappedByteBuffer bb = pipeChannel.map (FileChannel.MapMode.READ_WRITE, 0, 1);
                    bb.put (0, (octet) 1);
                    bb.force ();
                } catch (Throwable t) {
                    t.printStackTrace ();
                } enfin {
                    if (pipeChannel! = null) {
                        essayez {
                            pipeChannel.close ();
                        } catch (Throwable t) {
                            t.printStackTrace ();
                        }
                    } 
                }
                System.exit (0);
            }
            // Nous ne libérons pas le verrou et fermons le canal ici, 
            // qui sera fait après que l'application plante ou se ferme normalement. 
            SwingUtilities.invokeLater (
                nouveau Runnable () {
                    public void run () {
                        createAndShowGUI ();
                    }
                }
            );

            FileChannel pipeChannel = null;
            essayez {
                pipeChannel = new RandomAccessFile (PIPE, "rw"). getChannel ();
                MappedByteBuffer bb = pipeChannel.map (FileChannel.MapMode.READ_WRITE, 0, 1);
                while (vrai) {
                    octet b = bb.get (0);
                    si (b> 0) {
                        bb.put (0, (octet) 0);
                        bb.force ();
                        SwingUtilities.invokeLater (
                            nouveau Runnable () {
                                public void run () {
                                    frame.setExtendedState (JFrame.NORMAL);
                                    frame.setAlwaysOnTop (true);
                                    frame.toFront ();
                                    frame.setAlwaysOnTop (false);
                                }
                            }
                        );
                    }
                    Thread.sleep (1000);
                }
            } catch (Throwable t) {
                t.printStackTrace ();
            } enfin {
                if (pipeChannel! = null) {
                    essayez {
                        pipeChannel.close ();
                    } catch (Throwable t) {
                        t.printStackTrace ();
                    } 
                } 
            }
        } catch (Throwable t) {
            t.printStackTrace ();
        } 
    }

    public static void createAndShowGUI () {

        frame = new JFrame ();
        frame.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
        frame.setSize (800, 650);
        frame.getContentPane (). add (new JLabel ("MAIN WINDOW", 
                    SwingConstants.CENTER), BorderLayout.CENTER);
        frame.setLocationRelativeTo (null);
        frame.setVisible (vrai);
    }
}

George
la source
1

EDIT : Au lieu d'utiliser cette approche WatchService, un simple thread de minuterie de 1 seconde pourrait être utilisé pour vérifier si le paramètre IndicatorFile.exists (). Supprimez-le, puis placez l'application au premier plan ().

EDIT : Je voudrais savoir pourquoi cela a été déclassé. C'est la meilleure solution que j'ai vue jusqu'à présent. Par exemple, l'approche de socket serveur échoue si une autre application écoute déjà le port.

Téléchargez simplement Microsoft Windows Sysinternals TCPView (ou utilisez netstat), démarrez-le, triez par "État", recherchez le bloc de ligne qui dit "ÉCOUTE", choisissez celui dont l'adresse distante indique le nom de votre ordinateur, mettez ce port dans votre nouveau Socket ()-Solution. Dans ma mise en œuvre, je peux produire un échec à chaque fois. Et c'est logique , car c'est le fondement même de la démarche. Ou qu'est-ce que je ne reçois pas sur la façon de mettre en œuvre cela?

Veuillez m'informer si et comment je me trompe à ce sujet!

Mon opinion - que je vous demande de réfuter si possible - est que les développeurs sont invités à utiliser une approche dans le code de production qui échouera dans au moins 1 cas sur environ 60000. Et si ce point de vue s'avère correct, alors il est impossible qu'une solution présentée qui ne présente pas ce problème soit rejetée et critiquée pour sa quantité de code.

Inconvénients de l'approche socket en comparaison:

  • Échoue si le mauvais ticket de loterie (numéro de port) est choisi.
  • Échec dans un environnement multi-utilisateur: un seul utilisateur peut exécuter l'application à la fois. (Mon approche devrait être légèrement modifiée pour créer le (s) fichier (s) dans l'arborescence des utilisateurs, mais c'est trivial.)
  • Échoue si les règles de pare-feu sont trop strictes.
  • Les utilisateurs suspects (que j'ai rencontrés dans la nature) se demandent quelles manigances vous faites lorsque votre éditeur de texte revendique une socket serveur.

J'ai juste eu une bonne idée sur la façon de résoudre le problème de communication Java nouvelle-instance-instance-existante d'une manière qui devrait fonctionner sur tous les systèmes. Alors, j'ai organisé ce cours en environ deux heures. Fonctionne comme un charme: D

Il est basé sur l' approche de verrouillage de fichier de Robert (également sur cette page), que j'ai utilisée depuis. Pour indiquer à l'instance déjà en cours d'exécution qu'une autre instance a essayé de démarrer (mais ne l'a pas fait) ... un fichier est créé et immédiatement supprimé, et la première instance utilise WatchService pour détecter ce changement de contenu de dossier. Je n'arrive pas à croire qu'il s'agisse apparemment d'une nouvelle idée, étant donné la gravité du problème.

Cela peut facilement être changé pour simplement créer et non supprimer le fichier, puis des informations peuvent y être placées que l'instance appropriée peut évaluer, par exemple les arguments de la ligne de commande - et l'instance appropriée peut alors effectuer la suppression. Personnellement, je n'avais besoin que de savoir quand restaurer la fenêtre de mon application et l'envoyer au premier plan.

Exemple d'utilisation:

public static void main(final String[] args) {

    // ENSURE SINGLE INSTANCE
    if (!SingleInstanceChecker.INSTANCE.isOnlyInstance(Main::otherInstanceTriedToLaunch, false)) {
        System.exit(0);
    }

    // launch rest of application here
    System.out.println("Application starts properly because it's the only instance.");
}

private static void otherInstanceTriedToLaunch() {
    // Restore your application window and bring it to front.
    // But make sure your situation is apt: This method could be called at *any* time.
    System.err.println("Deiconified because other instance tried to start.");
}

Voici la classe:

package yourpackagehere;

import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;
import java.nio.file.*;




/**
 * SingleInstanceChecker v[(2), 2016-04-22 08:00 UTC] by dreamspace-president.com
 * <p>
 * (file lock single instance solution by Robert https://stackoverflow.com/a/2002948/3500521)
 */
public enum SingleInstanceChecker {

    INSTANCE; // HAHA! The CONFUSION!


    final public static int POLLINTERVAL = 1000;
    final public static File LOCKFILE = new File("SINGLE_INSTANCE_LOCKFILE");
    final public static File DETECTFILE = new File("EXTRA_INSTANCE_DETECTFILE");


    private boolean hasBeenUsedAlready = false;


    private WatchService watchService = null;
    private RandomAccessFile randomAccessFileForLock = null;
    private FileLock fileLock = null;


    /**
     * CAN ONLY BE CALLED ONCE.
     * <p>
     * Assumes that the program will close if FALSE is returned: The other-instance-tries-to-launch listener is not
     * installed in that case.
     * <p>
     * Checks if another instance is already running (temp file lock / shutdownhook). Depending on the accessibility of
     * the temp file the return value will be true or false. This approach even works even if the virtual machine
     * process gets killed. On the next run, the program can even detect if it has shut down irregularly, because then
     * the file will still exist. (Thanks to Robert https://stackoverflow.com/a/2002948/3500521 for that solution!)
     * <p>
     * Additionally, the method checks if another instance tries to start. In a crappy way, because as awesome as Java
     * is, it lacks some fundamental features. Don't worry, it has only been 25 years, it'll sure come eventually.
     *
     * @param codeToRunIfOtherInstanceTriesToStart Can be null. If not null and another instance tries to start (which
     *                                             changes the detect-file), the code will be executed. Could be used to
     *                                             bring the current (=old=only) instance to front. If null, then the
     *                                             watcher will not be installed at all, nor will the trigger file be
     *                                             created. (Null means that you just don't want to make use of this
     *                                             half of the class' purpose, but then you would be better advised to
     *                                             just use the 24 line method by Robert.)
     *                                             <p>
     *                                             BE CAREFUL with the code: It will potentially be called until the
     *                                             very last moment of the program's existence, so if you e.g. have a
     *                                             shutdown procedure or a window that would be brought to front, check
     *                                             if the procedure has not been triggered yet or if the window still
     *                                             exists / hasn't been disposed of yet. Or edit this class to be more
     *                                             comfortable. This would e.g. allow you to remove some crappy
     *                                             comments. Attribution would be nice, though.
     * @param executeOnAWTEventDispatchThread      Convenience function. If false, the code will just be executed. If
     *                                             true, it will be detected if we're currently on that thread. If so,
     *                                             the code will just be executed. If not so, the code will be run via
     *                                             SwingUtilities.invokeLater().
     * @return if this is the only instance
     */
    public boolean isOnlyInstance(final Runnable codeToRunIfOtherInstanceTriesToStart, final boolean executeOnAWTEventDispatchThread) {

        if (hasBeenUsedAlready) {
            throw new IllegalStateException("This class/method can only be used once, which kinda makes sense if you think about it.");
        }
        hasBeenUsedAlready = true;

        final boolean ret = canLockFileBeCreatedAndLocked();

        if (codeToRunIfOtherInstanceTriesToStart != null) {
            if (ret) {
                // Only if this is the only instance, it makes sense to install a watcher for additional instances.
                installOtherInstanceLaunchAttemptWatcher(codeToRunIfOtherInstanceTriesToStart, executeOnAWTEventDispatchThread);
            } else {
                // Only if this is NOT the only instance, it makes sense to create&delete the trigger file that will effect notification of the other instance.
                //
                // Regarding "codeToRunIfOtherInstanceTriesToStart != null":
                // While creation/deletion of the file concerns THE OTHER instance of the program,
                // making it dependent on the call made in THIS instance makes sense
                // because the code executed is probably the same.
                createAndDeleteOtherInstanceWatcherTriggerFile();
            }
        }

        optionallyInstallShutdownHookThatCleansEverythingUp();

        return ret;
    }


    private void createAndDeleteOtherInstanceWatcherTriggerFile() {

        try {
            final RandomAccessFile randomAccessFileForDetection = new RandomAccessFile(DETECTFILE, "rw");
            randomAccessFileForDetection.close();
            Files.deleteIfExists(DETECTFILE.toPath()); // File is created and then instantly deleted. Not a problem for the WatchService :)
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    private boolean canLockFileBeCreatedAndLocked() {

        try {
            randomAccessFileForLock = new RandomAccessFile(LOCKFILE, "rw");
            fileLock = randomAccessFileForLock.getChannel().tryLock();
            return fileLock != null;
        } catch (Exception e) {
            return false;
        }
    }


    private void installOtherInstanceLaunchAttemptWatcher(final Runnable codeToRunIfOtherInstanceTriesToStart, final boolean executeOnAWTEventDispatchThread) {

        // PREPARE WATCHSERVICE AND STUFF
        try {
            watchService = FileSystems.getDefault().newWatchService();
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
        final File appFolder = new File("").getAbsoluteFile(); // points to current folder
        final Path appFolderWatchable = appFolder.toPath();


        // REGISTER CURRENT FOLDER FOR WATCHING FOR FILE DELETIONS
        try {
            appFolderWatchable.register(watchService, StandardWatchEventKinds.ENTRY_DELETE);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }


        // INSTALL WATCHER THAT LOOKS IF OUR detectFile SHOWS UP IN THE DIRECTORY CHANGES. IF THERE'S A CHANGE, ANOTHER INSTANCE TRIED TO START, SO NOTIFY THE CURRENT ONE OF THAT.
        final Thread t = new Thread(() -> watchForDirectoryChangesOnExtraThread(codeToRunIfOtherInstanceTriesToStart, executeOnAWTEventDispatchThread));
        t.setDaemon(true);
        t.setName("directory content change watcher");
        t.start();
    }


    private void optionallyInstallShutdownHookThatCleansEverythingUp() {

        if (fileLock == null && randomAccessFileForLock == null && watchService == null) {
            return;
        }

        final Thread shutdownHookThread = new Thread(() -> {
            try {
                if (fileLock != null) {
                    fileLock.release();
                }
                if (randomAccessFileForLock != null) {
                    randomAccessFileForLock.close();
                }
                Files.deleteIfExists(LOCKFILE.toPath());
            } catch (Exception ignore) {
            }
            if (watchService != null) {
                try {
                    watchService.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
        Runtime.getRuntime().addShutdownHook(shutdownHookThread);
    }


    private void watchForDirectoryChangesOnExtraThread(final Runnable codeToRunIfOtherInstanceTriesToStart, final boolean executeOnAWTEventDispatchThread) {

        while (true) { // To eternity and beyond! Until the universe shuts down. (Should be a volatile boolean, but this class only has absolutely required features.)

            try {
                Thread.sleep(POLLINTERVAL);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }


            final WatchKey wk;
            try {
                wk = watchService.poll();
            } catch (ClosedWatchServiceException e) {
                // This situation would be normal if the watcher has been closed, but our application never does that.
                e.printStackTrace();
                return;
            }

            if (wk == null || !wk.isValid()) {
                continue;
            }


            for (WatchEvent<?> we : wk.pollEvents()) {

                final WatchEvent.Kind<?> kind = we.kind();
                if (kind == StandardWatchEventKinds.OVERFLOW) {
                    System.err.println("OVERFLOW of directory change events!");
                    continue;
                }


                final WatchEvent<Path> watchEvent = (WatchEvent<Path>) we;
                final File file = watchEvent.context().toFile();


                if (file.equals(DETECTFILE)) {

                    if (!executeOnAWTEventDispatchThread || SwingUtilities.isEventDispatchThread()) {
                        codeToRunIfOtherInstanceTriesToStart.run();
                    } else {
                        SwingUtilities.invokeLater(codeToRunIfOtherInstanceTriesToStart);
                    }

                    break;

                } else {
                    System.err.println("THIS IS THE FILE THAT WAS DELETED: " + file);
                }

            }

            wk.reset();
        }
    }

}
Président Dreamspace
la source
Vous n'avez pas besoin de centaines de lignes de code pour résoudre ce problème. new ServerSocket()avec un bloc de capture est tout à fait suffisant,
Marquis de Lorne
@EJP Faites-vous référence à la réponse acceptée ou de quoi parlez-vous? J'ai pas mal cherché une solution x-platform no-extra-library qui n'échoue pas, par exemple parce qu'une socket était déjà occupée par une application différente . S'il y a une solution à cela - particulièrement aussi simple que celle dont vous parlez - alors j'aimerais en savoir plus.
Dreamspace President
@EJP: Je veux demander à nouveau 1) à quelle solution triviale vous faites référence que vous avez suspendu comme une carotte devant ma tête, 2) au cas où c'est la solution socket la réponse acceptée commence par, si un ou plusieurs des mes puces "Inconvénients de l'approche socket" s'appliquent, et 3) si c'est le cas, pourquoi, malgré ces lacunes, vous recommanderiez toujours cette approche plutôt qu'une approche comme la mienne.
Dreamspace President
@EJP: Le problème est que votre voix a un certain poids, comme vous le savez certainement, mais toutes les preuves que j'ai me forcent à être convaincu que votre conseil ici est faux. Voyez, je n'insiste pas sur le fait que ma solution est juste et tout cela, mais je suis une machine basée sur des preuves. Ne voyez-vous pas que votre position vous donne la responsabilité envers la communauté de remplir les pièces manquantes du puzzle de cette communication que vous avez commencée?
Dreamspace President
@EJP: Comme il n'y a malheureusement pas eu de réaction de votre part, voici ce que je vais supposer comme un fait: la vérité sur la solution de socket serveur est qu'elle est vraiment très imparfaite , et la raison pour laquelle la plupart de ceux qui l'ont choisie pourraient avoir été "Les autres utilisez ceci aussi. », ou ils auraient pu être trompés en l’utilisant par des personnes irresponsables. Je suppose qu'une partie de la raison pour laquelle vous ne nous avez pas donné les explications requises est que vous ne pouvez pas comprendre cela / pourquoi vous n'avez jamais remis en question cette approche, et vous ne voulez pas faire une déclaration publique révélant cela.
Dreamspace President
1

La bibliothèque Unique4j peut être utilisée pour exécuter une seule instance d'une application Java et transmettre des messages. Vous pouvez le voir sur https://github.com/prat-man/unique4j . Il prend en charge Java 1.6+.

Il utilise une combinaison de verrous de fichiers et de verrous de port dynamiques pour détecter et communiquer entre les instances dans le but principal de n'autoriser qu'une seule instance à s'exécuter.

Voici un exemple simple de la même chose:

import tk.pratanumandal.unique4j.Unique4j;
import tk.pratanumandal.unique4j.exception.Unique4jException;

public class Unique4jDemo {

    // unique application ID
    public static String APP_ID = "tk.pratanumandal.unique4j-mlsdvo-20191511-#j.6";

    public static void main(String[] args) throws Unique4jException, InterruptedException {

        // create unique instance
        Unique4j unique = new Unique4j(APP_ID) {
            @Override
            public void receiveMessage(String message) {
                // display received message from subsequent instance
                System.out.println(message);
            }

            @Override
            public String sendMessage() {
                // send message to first instance
                return "Hello World!";
            }
        };

        // try to obtain lock
        boolean lockFlag = unique.acquireLock();

        // sleep the main thread for 30 seconds to simulate long running tasks
        Thread.sleep(30000);

        // try to free the lock before exiting program
        boolean lockFreeFlag = unique.freeLock();

    }

}

Avis de non-responsabilité: J'ai créé et entretenu la bibliothèque Unique4j.

Pratanu Mandal
la source