1 /* 2 * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package jdk.packager.services.singleton; 27 28 import java.awt.Desktop; 29 import java.awt.desktop.OpenFilesHandler; 30 import java.awt.desktop.OpenFilesEvent; 31 import java.net.ServerSocket; 32 import java.net.InetAddress; 33 import java.net.Socket; 34 import java.io.File; 35 import java.io.PrintStream; 36 import java.io.FileOutputStream; 37 import java.io.IOException; 38 import java.io.InputStream; 39 import java.io.BufferedReader; 40 import java.io.InputStreamReader; 41 import java.io.OutputStream; 42 import java.nio.charset.Charset; 43 import java.util.ArrayList; 44 import java.util.List; 45 import java.security.PrivilegedAction; 46 import java.security.AccessController; 47 import java.security.SecureRandom; 48 49 50 class SingleInstanceImpl { 51 52 static final String SI_FILEDIR = getTmpDir() + File.separator 53 + "si" + File.separator; 54 static final String SI_MAGICWORD = "javapackager.singleinstance.init"; 55 static final String SI_ACK = "javapackager.singleinstance.ack"; 56 static final String SI_STOP = "javapackager.singleinstance.stop"; 57 static final String SI_EOF = "javapackager.singleinstance.EOF"; 58 59 private final ArrayList<SingleInstanceListener> siListeners = new ArrayList<>(); 60 private SingleInstanceServer siServer; 61 62 private static final SecureRandom random = new SecureRandom(); 63 private static volatile boolean serverStarted = false; 64 private static int randomNumber; 65 66 private final Object lock = new Object(); 67 68 static String getSingleInstanceFilePrefix(final String stringId) { 69 String filePrefix = stringId.replace('/','_'); 70 filePrefix = filePrefix.replace(':','_'); 71 return filePrefix; 72 } 73 74 static String getTmpDir() { 75 String os = System.getProperty("os.name").toLowerCase(); 76 if (os.contains("win")) { 77 return System.getProperty("user.home") 78 + "\\AppData\\LocalLow\\Sun\\Java\\Packager\\tmp"; 79 } else if (os.contains("mac") || os.contains("os x")) { 80 return System.getProperty("user.home") 81 + "/Library/Application Support/Oracle/Java/Packager/tmp"; 82 } else if (os.contains("nix") || os.contains("nux") || os.contains("aix")) { 83 return System.getProperty("user.home") + "/.java/packager/tmp"; 84 } 85 86 return System.getProperty("java.io.tmpdir"); 87 } 88 89 void addSingleInstanceListener(SingleInstanceListener sil, String id) { 90 91 if (sil == null || id == null) { 92 return; 93 } 94 95 // start a new server thread for this unique id 96 // first time 97 synchronized (lock) { 98 if (!serverStarted) { 99 SingleInstanceService.trace("unique id: " + id); 100 try { 101 String sessionID = id + 102 SingleInstanceService.getSessionSpecificString(); 103 siServer = new SingleInstanceServer(sessionID); 104 siServer.start(); 105 } catch (Exception e) { 106 SingleInstanceService.trace("addSingleInstanceListener failed"); 107 SingleInstanceService.trace(e); 108 return; // didn't start 109 } 110 serverStarted = true; 111 } 112 } 113 114 synchronized (siListeners) { 115 // add the sil to the arrayList 116 if (!siListeners.contains(sil)) { 117 siListeners.add(sil); 118 } 119 } 120 121 } 122 123 class SingleInstanceServer { 124 125 private final SingleInstanceServerRunnable runnable; 126 private final Thread thread; 127 128 SingleInstanceServer(SingleInstanceServerRunnable runnable) throws IOException { 129 thread = new Thread(null, runnable, "JavaPackagerSIThread", 0, false); 130 thread.setDaemon(true); 131 this.runnable = runnable; 132 } 133 134 SingleInstanceServer(String stringId) throws IOException { 135 this(new SingleInstanceServerRunnable(stringId)); 136 } 137 138 int getPort() { 139 return runnable.getPort(); 140 } 141 142 void start() { 143 thread.start(); 144 } 145 } 146 147 private class SingleInstanceServerRunnable implements Runnable { 148 149 ServerSocket ss; 150 int port; 151 String stringId; 152 String[] arguments; 153 154 int getPort() { 155 return port; 156 } 157 158 SingleInstanceServerRunnable(String id) throws IOException { 159 stringId = id; 160 161 // open a free ServerSocket 162 ss = null; 163 164 // we should bind the server to the local InetAddress 127.0.0.1 165 // port number is automatically allocated for current SI 166 ss = new ServerSocket(0, 0, InetAddress.getByName("127.0.0.1")); 167 168 // get the port number 169 port = ss.getLocalPort(); 170 SingleInstanceService.trace("server port at: " + port); 171 172 // create the single instance file with canonical home and port number 173 createSingleInstanceFile(stringId, port); 174 } 175 176 private String getSingleInstanceFilename(final String id, final int port) { 177 String name = SI_FILEDIR + getSingleInstanceFilePrefix(id) + "_" + port; 178 SingleInstanceService.trace("getSingleInstanceFilename: " + name); 179 return name; 180 } 181 182 private void removeSingleInstanceFile(final String id, final int port) { 183 new File(getSingleInstanceFilename(id, port)).delete(); 184 SingleInstanceService.trace("removed SingleInstanceFile: " 185 + getSingleInstanceFilename(id, port)); 186 } 187 188 private void createSingleInstanceFile(final String id, final int port) { 189 String filename = getSingleInstanceFilename(id, port); 190 final File siFile = new File(filename); 191 final File siDir = new File(SI_FILEDIR); 192 AccessController.doPrivileged(new PrivilegedAction<Void>() { 193 @Override 194 public Void run() { 195 siDir.mkdirs(); 196 String[] fList = siDir.list(); 197 if (fList != null) { 198 String prefix = getSingleInstanceFilePrefix(id); 199 for (String file : fList) { 200 // if file with the same prefix already exist, remove it 201 if (file.startsWith(prefix)) { 202 SingleInstanceService.trace( 203 "file should be removed: " + SI_FILEDIR + file); 204 new File(SI_FILEDIR + file).delete(); 205 } 206 } 207 } 208 209 PrintStream out = null; 210 try { 211 siFile.createNewFile(); 212 siFile.deleteOnExit(); 213 // write random number to single instance file 214 out = new PrintStream(new FileOutputStream(siFile)); 215 randomNumber = random.nextInt(); 216 out.print(randomNumber); 217 } catch (IOException ioe) { 218 SingleInstanceService.trace(ioe); 219 } finally { 220 if (out != null) { 221 out.close(); 222 } 223 } 224 return null; 225 } 226 }); 227 } 228 229 @Override 230 public void run() { 231 // start sil to handle all the incoming request 232 // from the server port of the current url 233 AccessController.doPrivileged(new PrivilegedAction<Void>() { 234 @Override 235 public Void run() { 236 List<String> recvArgs = new ArrayList<>(); 237 while (true) { 238 recvArgs.clear(); 239 InputStream is = null; 240 BufferedReader in = null; 241 InputStreamReader isr = null; 242 Socket s = null; 243 String line = null; 244 boolean sendAck = false; 245 int port = -1; 246 String charset = null; 247 try { 248 SingleInstanceService.trace("waiting connection"); 249 s = ss.accept(); 250 is = s.getInputStream(); 251 // read first byte for encoding type 252 int encoding = is.read(); 253 if (encoding == SingleInstanceService.ENCODING_PLATFORM) { 254 charset = Charset.defaultCharset().name(); 255 } else if (encoding == SingleInstanceService.ENCODING_UNICODE) { 256 charset = SingleInstanceService.ENCODING_UNICODE_NAME; 257 } else { 258 SingleInstanceService.trace("SingleInstanceImpl - unknown encoding"); 259 return null; 260 } 261 isr = new InputStreamReader(is, charset); 262 in = new BufferedReader(isr); 263 // first read the random number 264 line = in.readLine(); 265 if (line.equals(String.valueOf(randomNumber)) == false) { 266 // random number does not match 267 // should not happen 268 // shutdown server socket 269 removeSingleInstanceFile(stringId, port); 270 ss.close(); 271 serverStarted = false; 272 SingleInstanceService.trace("Unexpected Error, " 273 + "SingleInstanceService disabled"); 274 return null; 275 } else { 276 line = in.readLine(); 277 // no need to continue reading if MAGICWORD 278 // did not come first 279 SingleInstanceService.trace("recv: " + line); 280 if (line.equals(SI_MAGICWORD)) { 281 SingleInstanceService.trace("got magic word."); 282 while (true) { 283 // Get input string 284 try { 285 line = in.readLine(); 286 if (line != null && line.equals(SI_EOF)) { 287 // end of file reached 288 break; 289 } else { 290 recvArgs.add(line); 291 } 292 } catch (IOException ioe) { 293 SingleInstanceService.trace(ioe); 294 } 295 } 296 arguments = recvArgs.toArray(new String[recvArgs.size()]); 297 sendAck = true; 298 } else if (line.equals(SI_STOP)) { 299 // remove the SingleInstance file 300 removeSingleInstanceFile(stringId, port); 301 break; 302 } 303 } 304 } catch (IOException ioe) { 305 SingleInstanceService.trace(ioe); 306 } finally { 307 try { 308 if (sendAck) { 309 // let the action listener handle the rest 310 for (String arg : arguments) { 311 SingleInstanceService.trace( 312 "Starting new instance with arguments: arg:" + arg); 313 } 314 315 performNewActivation(arguments); 316 317 // now the event is handled, we can send 318 // out the ACK 319 SingleInstanceService.trace("sending out ACK"); 320 if (s != null) { 321 try (OutputStream os = s.getOutputStream(); 322 PrintStream ps = new PrintStream(os, true, charset)) { 323 // send OK (ACK) 324 ps.println(SI_ACK); 325 ps.flush(); 326 } 327 } 328 } 329 330 if (in != null) { 331 in.close(); 332 } 333 334 if (isr != null) { 335 isr.close(); 336 } 337 338 if (is != null) { 339 is.close(); 340 } 341 342 if (s != null) { 343 s.close(); 344 } 345 } catch (IOException ioe) { 346 SingleInstanceService.trace(ioe); 347 } 348 } 349 } 350 return null; 351 } 352 }); 353 } 354 } 355 356 private void performNewActivation(final String[] args) { 357 // enumerate the sil list and call 358 // each sil with arguments 359 @SuppressWarnings("unchecked") 360 ArrayList<SingleInstanceListener> silal = 361 (ArrayList<SingleInstanceListener>)siListeners.clone(); 362 silal.forEach(sil -> sil.newActivation(args)); 363 } 364 365 void setOpenFileHandler() { 366 String os = System.getProperty("os.name").toLowerCase(); 367 if (!os.contains("mac") && !os.contains("os x")) { 368 return; 369 } 370 371 Desktop.getDesktop().setOpenFileHandler(new OpenFilesHandler() { 372 @Override 373 public void openFiles(OpenFilesEvent e) { 374 List<String> arguments = new ArrayList<>(); 375 e.getFiles().forEach(file -> arguments.add(file.toString())); 376 performNewActivation(arguments.toArray( 377 new String[arguments.size()])); 378 } 379 }); 380 } 381 382 void removeSingleInstanceListener(SingleInstanceListener sil) { 383 if (sil == null) { 384 return; 385 } 386 387 synchronized (siListeners) { 388 389 if (!siListeners.remove(sil)) { 390 return; 391 } 392 393 if (siListeners.isEmpty()) { 394 AccessController.doPrivileged(new PrivilegedAction<Void>() { 395 @Override 396 public Void run() { 397 // stop server 398 Socket socket = null; 399 PrintStream out = null; 400 OutputStream os = null; 401 try { 402 socket = new Socket("127.0.0.1", siServer.getPort()); 403 os = socket.getOutputStream(); 404 byte[] encoding = new byte[1]; 405 encoding[0] = SingleInstanceService.ENCODING_PLATFORM; 406 os.write(encoding); 407 String charset = Charset.defaultCharset().name(); 408 out = new PrintStream(os, true, charset); 409 out.println(randomNumber); 410 out.println(SingleInstanceImpl.SI_STOP); 411 out.flush(); 412 serverStarted = false; 413 } catch (IOException ioe) { 414 SingleInstanceService.trace(ioe); 415 } finally { 416 try { 417 if (out != null) { 418 out.close(); 419 } 420 if (os != null) { 421 os.close(); 422 } 423 if (socket != null) { 424 socket.close(); 425 } 426 } catch (IOException ioe) { 427 SingleInstanceService.trace(ioe); 428 } 429 } 430 return null; 431 } 432 }); 433 } 434 } 435 } 436 }