src/jdk.packager.services/share/classes/jdk/packager/services/singleton/SingleInstanceImpl.java
author herrick
Wed, 17 Oct 2018 13:50:11 -0400
branchJDK-8200758-branch
changeset 56982 e094d5483bd6
parent 56869 41e17fe9fbeb
permissions -rw-r--r--
8212048: Cleanup source code and fix issues discussed in preliminary review Reviewed-by: almatvee

/*
 * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package jdk.packager.services.singleton;

import java.awt.Desktop;
import java.awt.desktop.OpenFilesHandler;
import java.awt.desktop.OpenFilesEvent;
import java.net.ServerSocket;
import java.net.InetAddress;
import java.net.Socket;
import java.io.File;
import java.io.PrintStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.security.PrivilegedAction;
import java.security.AccessController;
import java.security.SecureRandom;


class SingleInstanceImpl {

    static final String SI_FILEDIR = getTmpDir() + File.separator
           + "si" + File.separator;
    static final String SI_MAGICWORD = "jpackager.singleinstance.init";
    static final String SI_ACK = "jpackager.singleinstance.ack";
    static final String SI_STOP = "jpackager.singleinstance.stop";
    static final String SI_EOF = "jpackager.singleinstance.EOF";

    private final ArrayList<SingleInstanceListener> siListeners =
            new ArrayList<>();
    private SingleInstanceServer siServer;

    private static final SecureRandom random = new SecureRandom();
    private static volatile boolean serverStarted = false;
    private static int randomNumber;

    private final Object lock = new Object();

    static String getSingleInstanceFilePrefix(final String stringId) {
        String filePrefix = stringId.replace('/','_');
        filePrefix = filePrefix.replace(':','_');
        return filePrefix;
    }

    static String getTmpDir() {
        String os = System.getProperty("os.name").toLowerCase();
        if (os.contains("win")) {
            return System.getProperty("user.home")
                    + "\\AppData\\LocalLow\\Sun\\Java\\Packager\\tmp";
        } else if (os.contains("mac") || os.contains("os x")) {
            return System.getProperty("user.home")
                    + "/Library/Application Support/Oracle/Java/Packager/tmp";
        } else if (os.contains("nix") || os.contains("nux")
                || os.contains("aix")) {
            return System.getProperty("user.home") + "/.java/packager/tmp";
        }

        return System.getProperty("java.io.tmpdir");
    }

    void addSingleInstanceListener(SingleInstanceListener sil, String id) {

        if (sil == null || id == null) {
            return;
        }

        // start a new server thread for this unique id
        // first time
        synchronized (lock) {
            if (!serverStarted) {
                SingleInstanceService.trace("unique id: " + id);
                try {
                    siServer = new SingleInstanceServer(id);
                    siServer.start();
                } catch (Exception e) {
                    SingleInstanceService.trace(
                            "addSingleInstanceListener failed");
                    SingleInstanceService.trace(e);
                    return; // didn't start
                }
                serverStarted = true;
            }
        }

        synchronized (siListeners) {
            // add the sil to the arrayList
            if (!siListeners.contains(sil)) {
                siListeners.add(sil);
            }
        }
    }

    class SingleInstanceServer {

        private final SingleInstanceServerRunnable runnable;
        private final Thread thread;

        SingleInstanceServer(SingleInstanceServerRunnable runnable)
                throws IOException {
            thread = new Thread(null, runnable, "JavaPackagerSIThread",
                                0, false);
            thread.setDaemon(true);
            this.runnable = runnable;
        }

        SingleInstanceServer(String stringId) throws IOException {
            this(new SingleInstanceServerRunnable(stringId));
        }

        int getPort() {
            return runnable.getPort();
        }

        void start() {
            thread.start();
        }
    }

    private class SingleInstanceServerRunnable implements Runnable {

        ServerSocket ss;
        int port;
        String stringId;
        String[] arguments;

        int getPort() {
            return port;
        }

        SingleInstanceServerRunnable(String id) throws IOException {
            stringId = id;

            // open a free ServerSocket
            ss = null;

            // we should bind the server to the local InetAddress 127.0.0.1
            // port number is automatically allocated for current SI
            ss = new ServerSocket(0, 0, InetAddress.getByName("127.0.0.1"));

            // get the port number
            port = ss.getLocalPort();
            SingleInstanceService.trace("server port at: " + port);

            // create the single instance file with canonical home and port num
            createSingleInstanceFile(stringId, port);
        }

        private String getSingleInstanceFilename(final String id,
                final int port) {
            String name = SI_FILEDIR + getSingleInstanceFilePrefix(id)
                    + "_" + port;
            SingleInstanceService.trace("getSingleInstanceFilename: " + name);
            return name;
        }

        private void removeSingleInstanceFile(final String id, final int port) {
            new File(getSingleInstanceFilename(id, port)).delete();
            SingleInstanceService.trace("removed SingleInstanceFile: "
                                        + getSingleInstanceFilename(id, port));
        }

        private void createSingleInstanceFile(final String id, final int port) {
            String filename = getSingleInstanceFilename(id, port);
            final File siFile = new File(filename);
            final File siDir = new File(SI_FILEDIR);
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                @Override
                public Void run() {
                    siDir.mkdirs();
                    String[] fList = siDir.list();
                    if (fList != null) {
                        String prefix = getSingleInstanceFilePrefix(id);
                        for (String file : fList) {
                            // if file with the same prefix exist, remove it
                            if (file.startsWith(prefix)) {
                                SingleInstanceService.trace(
                                        "file should be removed: "
                                         + SI_FILEDIR + file);
                                new File(SI_FILEDIR + file).delete();
                            }
                        }
                    }

                    PrintStream out = null;
                    try {
                        siFile.createNewFile();
                        siFile.deleteOnExit();
                        // write random number to single instance file
                        out = new PrintStream(new FileOutputStream(siFile));
                        randomNumber = random.nextInt();
                        out.print(randomNumber);
                    } catch (IOException ioe) {
                        SingleInstanceService.trace(ioe);
                    } finally {
                        if (out != null) {
                            out.close();
                        }
                    }
                    return null;
                }
            });
        }

        @Override
        public void run() {
            // start sil to handle all the incoming request
            // from the server port of the current url
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                @Override
                public Void run() {
                    List<String> recvArgs = new ArrayList<>();
                    while (true) {
                        recvArgs.clear();
                        InputStream is = null;
                        BufferedReader in = null;
                        InputStreamReader isr = null;
                        Socket s = null;
                        String line = null;
                        boolean sendAck = false;
                        int port = -1;
                        String charset = null;
                        try {
                            SingleInstanceService.trace("waiting connection");
                            s = ss.accept();
                            is = s.getInputStream();
                            // read first byte for encoding type
                            int encoding = is.read();
                            if (encoding ==
                                SingleInstanceService.ENCODING_PLATFORM) {
                                charset = Charset.defaultCharset().name();
                            } else if (encoding ==
                                    SingleInstanceService.ENCODING_UNICODE) {
                                charset =
                                    SingleInstanceService.ENCODING_UNICODE_NAME;
                            } else {
                                SingleInstanceService.trace(
                                    "SingleInstanceImpl - unknown encoding");
                                return null;
                            }
                            isr = new InputStreamReader(is, charset);
                            in = new BufferedReader(isr);
                            // first read the random number
                            line = in.readLine();
                            if (line.equals(String.valueOf(randomNumber)) ==
                                    false) {
                                // random number does not match
                                // should not happen
                                // shutdown server socket
                                removeSingleInstanceFile(stringId, port);
                                ss.close();
                                serverStarted = false;
                                SingleInstanceService.trace("Unexpected Error, "
                                        + "SingleInstanceService disabled");
                                return null;
                            } else {
                                line = in.readLine();
                                // no need to continue reading if MAGICWORD
                                // did not come first
                                SingleInstanceService.trace("recv: " + line);
                                if (line.equals(SI_MAGICWORD)) {
                                    SingleInstanceService.trace(
                                            "got magic word.");
                                    while (true) {
                                        // Get input string
                                        try {
                                            line = in.readLine();
                                            if (line != null
                                                    && line.equals(SI_EOF)) {
                                                // end of file reached
                                                break;
                                            } else {
                                                recvArgs.add(line);
                                            }
                                        } catch (IOException ioe) {
                                            SingleInstanceService.trace(ioe);
                                        }
                                    }
                                    arguments = recvArgs.toArray(
                                            new String[recvArgs.size()]);
                                    sendAck = true;
                                } else if (line.equals(SI_STOP)) {
                                    // remove the SingleInstance file
                                    removeSingleInstanceFile(stringId, port);
                                    break;
                                }
                            }
                        } catch (IOException ioe) {
                            SingleInstanceService.trace(ioe);
                        } finally {
                            try {
                                if (sendAck) {
                                    // let the action listener handle the rest
                                    for (String arg : arguments) {
                                        SingleInstanceService.trace(
                                                "Starting new instance with "
                                                + "arguments: arg:" + arg);
                                    }

                                    performNewActivation(arguments);

                                    // now the event is handled, we can send
                                    // out the ACK
                                    SingleInstanceService.trace(
                                            "sending out ACK");
                                    if (s != null) {
                                        try (OutputStream os =
                                                s.getOutputStream();
                                            PrintStream ps = new PrintStream(os,
                                                    true, charset)) {
                                            // send OK (ACK)
                                            ps.println(SI_ACK);
                                            ps.flush();
                                        }
                                    }
                                }

                                if (in != null) {
                                    in.close();
                                }

                                if (isr != null) {
                                    isr.close();
                                }

                                if (is != null) {
                                    is.close();
                                }

                                if (s != null) {
                                    s.close();
                                }
                            } catch (IOException ioe) {
                                SingleInstanceService.trace(ioe);
                            }
                        }
                    }
                    return null;
                }
            });
        }
    }

    private void performNewActivation(final String[] args) {
        // enumerate the sil list and call
        // each sil with arguments
        @SuppressWarnings("unchecked")
        ArrayList<SingleInstanceListener> silal =
                (ArrayList<SingleInstanceListener>)siListeners.clone();
        silal.forEach(sil -> sil.newActivation(args));
    }

    void setOpenFileHandler() {
        String os = System.getProperty("os.name").toLowerCase();
        if (!os.contains("mac") && !os.contains("os x")) {
            return;
        }

        Desktop.getDesktop().setOpenFileHandler(new OpenFilesHandler() {
            @Override
            public void openFiles(OpenFilesEvent e) {
                List<String> arguments = new ArrayList<>();
                e.getFiles().forEach(file -> arguments.add(file.toString()));
                performNewActivation(arguments.toArray(
                                    new String[arguments.size()]));
            }
        });
    }

    void removeSingleInstanceListener(SingleInstanceListener sil) {
        if (sil == null) {
            return;
        }

        synchronized (siListeners) {

            if (!siListeners.remove(sil)) {
                return;
            }

            if (siListeners.isEmpty()) {
                 AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    @Override
                    public Void run() {
                        // stop server
                        Socket socket = null;
                        PrintStream out = null;
                        OutputStream os = null;
                        try {
                            socket = new Socket("127.0.0.1",
                                    siServer.getPort());
                            os = socket.getOutputStream();
                            byte[] encoding = new byte[1];
                            encoding[0] =
                                    SingleInstanceService.ENCODING_PLATFORM;
                            os.write(encoding);
                            String charset = Charset.defaultCharset().name();
                            out = new PrintStream(os, true, charset);
                            out.println(randomNumber);
                            out.println(SingleInstanceImpl.SI_STOP);
                            out.flush();
                            serverStarted = false;
                        } catch (IOException ioe) {
                            SingleInstanceService.trace(ioe);
                        } finally {
                            try {
                                if (out != null) {
                                    out.close();
                                }
                                if (os != null) {
                                    os.close();
                                }
                                if (socket != null) {
                                    socket.close();
                                }
                            } catch (IOException ioe) {
                                SingleInstanceService.trace(ioe);
                            }
                        }
                        return null;
                    }
               });
            }
        }
    }
}