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 }