1 /* 2 * Copyright (c) 2017, 2018, 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.jpackager.runtime.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 = "jpackager.singleinstance.init"; 55 static final String SI_ACK = "jpackager.singleinstance.ack"; 56 static final String SI_STOP = "jpackager.singleinstance.stop"; 57 static final String SI_EOF = "jpackager.singleinstance.EOF"; 58 59 private final ArrayList<SingleInstanceListener> siListeners = 60 new ArrayList<>(); 61 private SingleInstanceServer siServer; 62 63 private static final SecureRandom random = new SecureRandom(); 64 private static volatile boolean serverStarted = false; 65 private static int randomNumber; 66 67 private final Object lock = new Object(); 68 69 static String getSingleInstanceFilePrefix(final String stringId) { 70 String filePrefix = stringId.replace('/','_'); 71 filePrefix = filePrefix.replace(':','_'); 72 return filePrefix; 73 } 74 75 static String getTmpDir() { 76 String os = System.getProperty("os.name").toLowerCase(); 77 if (os.contains("win")) { 78 return System.getProperty("user.home") 79 + "\\AppData\\LocalLow\\Sun\\Java\\JPackager\\tmp"; 80 } else if (os.contains("mac") || os.contains("os x")) { 81 return System.getProperty("user.home") 82 + "/Library/Application Support/Oracle/Java/JPackager/tmp"; 83 } else if (os.contains("nix") || os.contains("nux") 84 || os.contains("aix")) { 85 return System.getProperty("user.home") + "/.java/jpackager/tmp"; 86 } 87 88 return System.getProperty("java.io.tmpdir"); 89 } 90 91 void addSingleInstanceListener(SingleInstanceListener sil, String id) { 92 93 if (sil == null || id == null) { 94 return; 95 } 96 97 // start a new server thread for this unique id 98 // first time 99 synchronized (lock) { 100 if (!serverStarted) { 101 SingleInstanceService.trace("unique id: " + id); 102 try { 103 siServer = new SingleInstanceServer(id); 104 siServer.start(); 105 } catch (Exception e) { 106 SingleInstanceService.trace( 107 "addSingleInstanceListener failed"); 108 SingleInstanceService.trace(e); 109 return; // didn't start 110 } 111 serverStarted = true; 112 } 113 } 114 115 synchronized (siListeners) { 116 // add the sil to the arrayList 117 if (!siListeners.contains(sil)) { 118 siListeners.add(sil); 119 } 120 } 121 } 122 123 class SingleInstanceServer { 124 125 private final SingleInstanceServerRunnable runnable; 126 private final Thread thread; 127 128 SingleInstanceServer(SingleInstanceServerRunnable runnable) 129 throws IOException { 130 thread = new Thread(null, runnable, "JPackagerSIThread", 131 0, false); 132 thread.setDaemon(true); 133 this.runnable = runnable; 134 } 135 136 SingleInstanceServer(String stringId) throws IOException { 137 this(new SingleInstanceServerRunnable(stringId)); 138 } 139 140 int getPort() { 141 return runnable.getPort(); 142 } 143 144 void start() { 145 thread.start(); 146 } 147 } 148 149 private class SingleInstanceServerRunnable implements Runnable { 150 151 ServerSocket ss; 152 int port; 153 String stringId; 154 String[] arguments; 155 156 int getPort() { 157 return port; 158 } 159 160 SingleInstanceServerRunnable(String id) throws IOException { 161 stringId = id; 162 163 // open a free ServerSocket 164 ss = null; 165 166 // we should bind the server to the local InetAddress 127.0.0.1 167 // port number is automatically allocated for current SI 168 ss = new ServerSocket(0, 0, InetAddress.getByName("127.0.0.1")); 169 170 // get the port number 171 port = ss.getLocalPort(); 172 SingleInstanceService.trace("server port at: " + port); 173 174 // create the single instance file with canonical home and port num 175 createSingleInstanceFile(stringId, port); 176 } 177 178 private String getSingleInstanceFilename(final String id, 179 final int port) { 180 String name = SI_FILEDIR + getSingleInstanceFilePrefix(id) 181 + "_" + port; 182 SingleInstanceService.trace("getSingleInstanceFilename: " + name); 183 return name; 184 } 185 186 private void removeSingleInstanceFile(final String id, final int port) { 187 new File(getSingleInstanceFilename(id, port)).delete(); 188 SingleInstanceService.trace("removed SingleInstanceFile: " 189 + getSingleInstanceFilename(id, port)); 190 } 191 192 private void createSingleInstanceFile(final String id, final int port) { 193 String filename = getSingleInstanceFilename(id, port); 194 final File siFile = new File(filename); 195 final File siDir = new File(SI_FILEDIR); 196 AccessController.doPrivileged(new PrivilegedAction<Void>() { 197 @Override 198 public Void run() { 199 siDir.mkdirs(); 200 String[] fList = siDir.list(); 201 if (fList != null) { 202 String prefix = getSingleInstanceFilePrefix(id); 203 for (String file : fList) { 204 // if file with the same prefix exist, remove it 205 if (file.startsWith(prefix)) { 206 SingleInstanceService.trace( 207 "file should be removed: " 208 + SI_FILEDIR + file); 209 new File(SI_FILEDIR + file).delete(); 210 } 211 } 212 } 213 214 PrintStream out = null; 215 try { 216 siFile.createNewFile(); 217 siFile.deleteOnExit(); 218 // write random number to single instance file 219 out = new PrintStream(new FileOutputStream(siFile)); 220 randomNumber = random.nextInt(); 221 out.print(randomNumber); 222 } catch (IOException ioe) { 223 SingleInstanceService.trace(ioe); 224 } finally { 225 if (out != null) { 226 out.close(); 227 } 228 } 229 return null; 230 } 231 }); 232 } 233 234 @Override 235 public void run() { 236 // start sil to handle all the incoming request 237 // from the server port of the current url 238 AccessController.doPrivileged(new PrivilegedAction<Void>() { 239 @Override 240 public Void run() { 241 List<String> recvArgs = new ArrayList<>(); 242 while (true) { 243 recvArgs.clear(); 244 InputStream is = null; 245 BufferedReader in = null; 246 InputStreamReader isr = null; 247 Socket s = null; 248 String line = null; 249 boolean sendAck = false; 250 int port = -1; 251 String charset = null; 252 try { 253 SingleInstanceService.trace("waiting connection"); 254 s = ss.accept(); 255 is = s.getInputStream(); 256 // read first byte for encoding type 257 int encoding = is.read(); 258 if (encoding == 259 SingleInstanceService.ENCODING_PLATFORM) { 260 charset = Charset.defaultCharset().name(); 261 } else if (encoding == 262 SingleInstanceService.ENCODING_UNICODE) { 263 charset = 264 SingleInstanceService.ENCODING_UNICODE_NAME; 265 } else { 266 SingleInstanceService.trace( 267 "SingleInstanceImpl - unknown encoding"); 268 return null; 269 } 270 isr = new InputStreamReader(is, charset); 271 in = new BufferedReader(isr); 272 // first read the random number 273 line = in.readLine(); 274 if (line.equals(String.valueOf(randomNumber)) == 275 false) { 276 // random number does not match 277 // should not happen 278 // shutdown server socket 279 removeSingleInstanceFile(stringId, port); 280 ss.close(); 281 serverStarted = false; 282 SingleInstanceService.trace("Unexpected Error, " 283 + "SingleInstanceService disabled"); 284 return null; 285 } else { 286 line = in.readLine(); 287 // no need to continue reading if MAGICWORD 288 // did not come first 289 SingleInstanceService.trace("recv: " + line); 290 if (line.equals(SI_MAGICWORD)) { 291 SingleInstanceService.trace( 292 "got magic word."); 293 while (true) { 294 // Get input string 295 try { 296 line = in.readLine(); 297 if (line != null 298 && line.equals(SI_EOF)) { 299 // end of file reached 300 break; 301 } else { 302 recvArgs.add(line); 303 } 304 } catch (IOException ioe) { 305 SingleInstanceService.trace(ioe); 306 } 307 } 308 arguments = recvArgs.toArray( 309 new String[recvArgs.size()]); 310 sendAck = true; 311 } else if (line.equals(SI_STOP)) { 312 // remove the SingleInstance file 313 removeSingleInstanceFile(stringId, port); 314 break; 315 } 316 } 317 } catch (IOException ioe) { 318 SingleInstanceService.trace(ioe); 319 } finally { 320 try { 321 if (sendAck) { 322 // let the action listener handle the rest 323 for (String arg : arguments) { 324 SingleInstanceService.trace( 325 "Starting new instance with " 326 + "arguments: arg:" + arg); 327 } 328 329 performNewActivation(arguments); 330 331 // now the event is handled, we can send 332 // out the ACK 333 SingleInstanceService.trace( 334 "sending out ACK"); 335 if (s != null) { 336 try (OutputStream os = 337 s.getOutputStream(); 338 PrintStream ps = new PrintStream(os, 339 true, charset)) { 340 // send OK (ACK) 341 ps.println(SI_ACK); 342 ps.flush(); 343 } 344 } 345 } 346 347 if (in != null) { 348 in.close(); 349 } 350 351 if (isr != null) { 352 isr.close(); 353 } 354 355 if (is != null) { 356 is.close(); 357 } 358 359 if (s != null) { 360 s.close(); 361 } 362 } catch (IOException ioe) { 363 SingleInstanceService.trace(ioe); 364 } 365 } 366 } 367 return null; 368 } 369 }); 370 } 371 } 372 373 private void performNewActivation(final String[] args) { 374 // enumerate the sil list and call 375 // each sil with arguments 376 @SuppressWarnings("unchecked") 377 ArrayList<SingleInstanceListener> silal = 378 (ArrayList<SingleInstanceListener>)siListeners.clone(); 379 silal.forEach(sil -> sil.newActivation(args)); 380 } 381 382 void setOpenFileHandler() { 383 String os = System.getProperty("os.name").toLowerCase(); 384 if (!os.contains("mac") && !os.contains("os x")) { 385 return; 386 } 387 388 Desktop.getDesktop().setOpenFileHandler(new OpenFilesHandler() { 389 @Override 390 public void openFiles(OpenFilesEvent e) { 391 List<String> arguments = new ArrayList<>(); 392 e.getFiles().forEach(file -> arguments.add(file.toString())); 393 performNewActivation(arguments.toArray( 394 new String[arguments.size()])); 395 } 396 }); 397 } 398 399 void removeSingleInstanceListener(SingleInstanceListener sil) { 400 if (sil == null) { 401 return; 402 } 403 404 synchronized (siListeners) { 405 406 if (!siListeners.remove(sil)) { 407 return; 408 } 409 410 if (siListeners.isEmpty()) { 411 AccessController.doPrivileged(new PrivilegedAction<Void>() { 412 @Override 413 public Void run() { 414 // stop server 415 Socket socket = null; 416 PrintStream out = null; 417 OutputStream os = null; 418 try { 419 socket = new Socket("127.0.0.1", 420 siServer.getPort()); 421 os = socket.getOutputStream(); 422 byte[] encoding = new byte[1]; 423 encoding[0] = 424 SingleInstanceService.ENCODING_PLATFORM; 425 os.write(encoding); 426 String charset = Charset.defaultCharset().name(); 427 out = new PrintStream(os, true, charset); 428 out.println(randomNumber); 429 out.println(SingleInstanceImpl.SI_STOP); 430 out.flush(); 431 serverStarted = false; 432 } catch (IOException ioe) { 433 SingleInstanceService.trace(ioe); 434 } finally { 435 try { 436 if (out != null) { 437 out.close(); 438 } 439 if (os != null) { 440 os.close(); 441 } 442 if (socket != null) { 443 socket.close(); 444 } 445 } catch (IOException ioe) { 446 SingleInstanceService.trace(ioe); 447 } 448 } 449 return null; 450 } 451 }); 452 } 453 } 454 } 455 }