src/jdk.packager.services/share/classes/jdk/packager/services/singleton/SingleInstanceImpl.java
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;
}
});
}
}
}
}