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 }