How to implement a single instance Java application?

后端 未结 17 770
北荒
北荒 2020-11-22 04:32

Sometime I see many application such as msn, windows media player etc that are single instance applications (when user executes while application is running a new applicatio

相关标签:
17条回答
  • 2020-11-22 04:46

    EDIT: Instead of using this WatchService approach, a simple 1 second timer thread could be used to check if the indicatorFile.exists(). Delete it, then bring the application toFront().

    EDIT: I would like to know why this was downvoted. It's the best solution I have seen so far. E.g. the server socket approach fails if another application happens to already be listening to the port.

    Just download Microsoft Windows Sysinternals TCPView (or use netstat), start it, sort by "State", look for the line block that says "LISTENING", pick one whose remote address says your computer's name, put that port into your new-Socket()-solution. In my implementation of it, I can produce failure every time. And it's logical, because it's the very foundation of the approach. Or what am I not getting regarding how to implement this?

    Please inform me if and how I am wrong about this!

    My view - which I am asking you to disprove if possible - is that developers are being advised to use an approach in production code that will fail in at least 1 of about 60000 cases. And if this view happens to be correct, then it can absolutely not be that a solution presented that does not have this problem is downvoted and criticized for its amount of code.

    Disadvantages of the socket approach in comparison:

    • Fails if the wrong lottery ticket (port number) is chosen.
    • Fails in multi user environment: Only one user can run the application at the same time. (My approach would have to be slightly changed to create the file(s) in the user tree, but that's trivial.)
    • Fails if firewall rules are too strict.
    • Makes suspicious users (which I did meet in the wild) wonder what shenanigans you're up to when your text editor is claiming a server socket.

    I just had a nice idea for how to solve the new-instance-to-existing-instance Java communication problem in a way that should work on every system. So, I whipped up this class in about two hours. Works like a charm :D

    It's based on Robert's file lock approach (also on this page), which I have used ever since. To tell the already running instance that another instance tried to start (but didn't) ... a file is created and immediately deleted, and the first instance uses the WatchService to detect this folder content change. I can't believe that apparently this is a new idea, given how fundamental the problem is.

    This can easily be changed to just create and not delete the file, and then information can be put into it that the proper instance can evaluate, e.g. the command line arguments - and the proper instance can then perform the deletion. Personally, I only needed to know when to restore my application's window and send it to front.

    Example use:

    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.");
    }
    

    Here's the class:

    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();
            }
        }
    
    }
    
    0 讨论(0)
  • 2020-11-22 04:47

    ManagementFactory class supported in J2SE 5.0 or later detail

    but now i use J2SE 1.4 and I found this one http://audiprimadhanty.wordpress.com/2008/06/30/ensuring-one-instance-of-application-running-at-one-time/ but I never test. What do you think about it?

    0 讨论(0)
  • 2020-11-22 04:47

    I used sockets for that and depending if the application is on the client side or server side the behavior is a bit different:

    • client side : if an instance already exists(I cannot listen on a specific port) I will pass the application parameters and exit(you may want to perform some actions in the previous instance) if not I will start the application.
    • server side : if an instance already exists I will print a message and exit, if not I will start the application.
    0 讨论(0)
  • 2020-11-22 04:48

    I use the following method in the main method. This is the simplest, most robust, and least intrusive method I have seen so I thought that I'd share it.

    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;
    }
    
    0 讨论(0)
  • 2020-11-22 04:50

    We use file locking for this (grab an exclusive lock on a magic file in the user's app data directory), but we are primarily interested in preventing multiple instances from ever running.

    If you are trying to have the second instance pass command line args, etc... to the first instance, then using a socket connection on localhost will be killing two birds with one stone. General algorithm:

    • On launch, try to open listener on port XXXX on localhost
    • if fail, open a writer to that port on localhost and send the command line args, then shutdown
    • otherwise, listen on port XXXXX on localhost. When receive command line args, process them as if the app was launched with that command line.
    0 讨论(0)
  • 2020-11-22 04:51

    Yes this is a really decent answer for eclipse RCP eclipse single instance application below is my code

    in 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;
    }
    
    0 讨论(0)
提交回复
热议问题