1 /*
   2  * Copyright (c) 2014, 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 com.sun.tools.sjavac.server;
  27 
  28 import java.io.BufferedReader;
  29 import java.io.File;
  30 import java.io.FileReader;
  31 import java.io.IOException;
  32 import java.io.InputStreamReader;
  33 import java.io.PrintStream;
  34 import java.io.PrintWriter;
  35 import java.io.StringWriter;
  36 import java.net.InetAddress;
  37 import java.net.InetSocketAddress;
  38 import java.net.Socket;
  39 import java.net.SocketAddress;
  40 import java.net.URI;
  41 import java.util.Collections;
  42 import java.util.HashMap;
  43 import java.util.HashSet;
  44 import java.util.List;
  45 import java.util.Map;
  46 import java.util.Set;
  47 
  48 import com.sun.tools.sjavac.Util;
  49 import com.sun.tools.sjavac.options.Options;
  50 
  51 import static com.sun.tools.sjavac.server.CompilationResult.ERROR_BUT_TRY_AGAIN;
  52 import static com.sun.tools.sjavac.server.CompilationResult.ERROR_FATAL;
  53 
  54 public class JavacServiceClient implements JavacService {
  55 
  56 
  57     // The id can perhaps be used in the future by the javac server to reuse the
  58     // JavaCompiler instance for several compiles using the same id.
  59     private final String id;
  60     private final String portfile;
  61     private final String logfile;
  62     private final String stdouterrfile;
  63     private final boolean background;
  64 
  65     // Default keepalive for server is 120 seconds.
  66     // I.e. it will accept 120 seconds of inactivity before quitting.
  67     private final int keepalive;
  68     private final int poolsize;
  69 
  70     // The sjavac option specifies how the server part of sjavac is spawned.
  71     // If you have the experimental sjavac in your path, you are done. If not, you have
  72     // to point to a com.sun.tools.sjavac.Main that supports --startserver
  73     // for example by setting: sjavac=java%20-jar%20...javac.jar%com.sun.tools.sjavac.Main
  74     private final String sjavac;
  75 
  76     // Store the server conf settings here.
  77     private final String settings;
  78 
  79     public JavacServiceClient(Options options) {
  80         String tmpServerConf = options.getServerConf();
  81         String serverConf = (tmpServerConf!=null)? tmpServerConf : "";
  82         String tmpId = Util.extractStringOption("id", serverConf);
  83         id = (tmpId!=null) ? tmpId : "id"+(((new java.util.Random()).nextLong())&Long.MAX_VALUE);
  84         String p = Util.extractStringOption("portfile", serverConf);
  85         portfile = (p!=null) ? p : options.getStateDir().toFile().getAbsolutePath()+File.separatorChar+"javac_server";
  86         logfile = Util.extractStringOption("logfile", serverConf, portfile + ".javaclog");
  87         stdouterrfile = Util.extractStringOption("stdouterrfile", serverConf, portfile + ".stdouterr");
  88         background = Util.extractBooleanOption("background", serverConf, true);
  89         sjavac = Util.extractStringOption("sjavac", serverConf, "sjavac");
  90         int poolsize = Util.extractIntOption("poolsize", serverConf);
  91         keepalive = Util.extractIntOption("keepalive", serverConf, 120);
  92 
  93         this.poolsize = poolsize > 0 ? poolsize : Runtime.getRuntime().availableProcessors();
  94         settings = (serverConf.equals("")) ? "id="+id+",portfile="+portfile : serverConf;
  95     }
  96 
  97     /**
  98      * Hand out the server settings.
  99      * @return The server settings, possibly a default value.
 100      */
 101     public String serverSettings() {
 102         return settings;
 103     }
 104 
 105     /**
 106      * Make a request to the server only to get the maximum possible heap size to use for compilations.
 107      *
 108      * @param port_file The port file used to synchronize creation of this server.
 109      * @param id The identify of the compilation.
 110      * @param out Standard out information.
 111      * @param err Standard err information.
 112      * @return The maximum heap size in bytes.
 113      */
 114     @Override
 115     public SysInfo getSysInfo() {
 116         try {
 117             CompilationResult cr = useServer(new String[0],
 118                                              Collections.<URI>emptySet(),
 119                                              Collections.<URI>emptySet(),
 120                                              Collections.<URI, Set<String>>emptyMap());
 121             return cr.sysinfo;
 122         } catch (Exception e) {
 123             return new SysInfo(-1, -1);
 124         }
 125     }
 126 
 127     @Override
 128     public CompilationResult compile(String protocolId,
 129                                      String invocationId,
 130                                      String[] args,
 131                                      List<File> explicitSources,
 132                                      Set<URI> sourcesToCompile,
 133                                      Set<URI> visibleSources) {
 134         // Delegate to useServer, which delegates to compileHelper
 135         return useServer(args, sourcesToCompile, visibleSources, null);
 136     }
 137 
 138     /**
 139      * Connect and compile using the javac server settings and the args. When using more advanced features, the sources_to_compile and visible_sources are
 140      * supplied to the server and meta data is returned in package_artifacts, package_dependencies and package_pubapis.
 141      */
 142     public CompilationResult compileHelper(String id,
 143                                            String[] args,
 144                                            Set<URI> sourcesToCompile,
 145                                            Set<URI> visibleSources) {
 146 
 147         CompilationResult rc = new CompilationResult(-3);
 148 
 149         try {
 150             PortFile portFile = JavacServer.getPortFile(this.portfile);
 151 
 152             int port = portFile.containsPortInfo() ? portFile.getPort() : 0;
 153             if (port == 0) {
 154                 return new CompilationResult(ERROR_BUT_TRY_AGAIN);
 155             }
 156             long cookie = portFile.getCookie();
 157             // Acquire the localhost/127.0.0.1 address.
 158             InetAddress addr = InetAddress.getByName(null);
 159             SocketAddress sockaddr = new InetSocketAddress(addr, port);
 160             Socket sock = new Socket();
 161             int timeoutMs = JavacServer.CONNECTION_TIMEOUT * 1000;
 162             try {
 163                 sock.connect(sockaddr, timeoutMs);
 164             } catch (java.net.ConnectException e) {
 165                 rc.setReturnCode(ERROR_BUT_TRY_AGAIN);
 166                 rc.stderr = "Could not connect to javac server found in portfile: " + portFile.getFilename() + " " + e;
 167                 return rc;
 168             }
 169             if (!sock.isConnected()) {
 170                 rc.setReturnCode(ERROR_BUT_TRY_AGAIN);
 171                 rc.stderr = "Could not connect to javac server found in portfile: " + portFile.getFilename();
 172                 return rc;
 173             }
 174 
 175             //
 176             // Send arguments
 177             //
 178             BufferedReader in = new BufferedReader(new InputStreamReader(sock.getInputStream()));
 179             PrintWriter sockout = new PrintWriter(sock.getOutputStream());
 180 
 181             sockout.println(JavacServer.PROTOCOL_COOKIE_VERSION);
 182             sockout.println("" + cookie);
 183             sockout.println(JavacServer.PROTOCOL_CWD);
 184             sockout.println(System.getProperty("user.dir"));
 185             sockout.println(JavacServer.PROTOCOL_ID);
 186             sockout.println(id);
 187             sockout.println(JavacServer.PROTOCOL_ARGS);
 188             for (String s : args) {
 189                 StringBuffer buf = new StringBuffer();
 190                 String[] paths = s.split(File.pathSeparator);
 191                 int c = 0;
 192                 for (String path : paths) {
 193                     File f = new File(path);
 194                     if (f.isFile() || f.isDirectory()) {
 195                         buf.append(f.getAbsolutePath());
 196                         c++;
 197                         if (c < paths.length) {
 198                             buf.append(File.pathSeparator);
 199                         }
 200                     } else {
 201                         buf = new StringBuffer(s);
 202                         break;
 203                     }
 204                 }
 205                 sockout.println(buf.toString());
 206             }
 207             sockout.println(JavacServer.PROTOCOL_SOURCES_TO_COMPILE);
 208             for (URI uri : sourcesToCompile) {
 209                 sockout.println(uri.toString());
 210             }
 211             sockout.println(JavacServer.PROTOCOL_VISIBLE_SOURCES);
 212             for (URI uri : visibleSources) {
 213                 sockout.println(uri.toString());
 214             }
 215             sockout.println(JavacServer.PROTOCOL_END);
 216             sockout.flush();
 217 
 218             //
 219             // Receive result
 220             //
 221             StringBuffer stdout = new StringBuffer();
 222             StringBuffer stderr = new StringBuffer();
 223 
 224             if (!JavacServiceClient.expect(in, JavacServer.PROTOCOL_STDOUT)) {
 225                 return new CompilationResult(ERROR_FATAL);
 226             }
 227             // Load stdout
 228             for (;;) {
 229                 String l = in.readLine();
 230                 if (l == null) {
 231                     return new CompilationResult(ERROR_FATAL);
 232                 }
 233                 if (l.equals(JavacServer.PROTOCOL_STDERR)) {
 234                     break;
 235                 }
 236                 stdout.append(l);
 237                 stdout.append('\n');
 238             }
 239             // Load stderr
 240             for (;;) {
 241                 String l = in.readLine();
 242                 if (l == null) {
 243                     return new CompilationResult(ERROR_FATAL);
 244                 }
 245                 if (l.equals(JavacServer.PROTOCOL_PACKAGE_ARTIFACTS)) {
 246                     break;
 247                 }
 248                 stderr.append(l);
 249                 stderr.append('\n');
 250             }
 251             // Load the package artifacts
 252             Set<URI> lastUriSet = null;
 253             for (;;) {
 254                 String l = in.readLine();
 255                 if (l == null) {
 256                     return new CompilationResult(ERROR_FATAL);
 257                 }
 258                 if (l.equals(JavacServer.PROTOCOL_PACKAGE_DEPENDENCIES)) {
 259                     break;
 260                 }
 261                 if (l.length() > 1 && l.charAt(0) == '+') {
 262                     String pkg = l.substring(1);
 263                     lastUriSet = new HashSet<>();
 264                     rc.packageArtifacts.put(pkg, lastUriSet);
 265                 } else if (l.length() > 1 && lastUriSet != null) {
 266                     lastUriSet.add(new URI(l.substring(1)));
 267                 }
 268             }
 269             // Load package dependencies
 270             Set<String> lastPackageSet = null;
 271             for (;;) {
 272                 String l = in.readLine();
 273                 if (l == null) {
 274                     return new CompilationResult(ERROR_FATAL);
 275                 }
 276                 if (l.equals(JavacServer.PROTOCOL_PACKAGE_PUBLIC_APIS)) {
 277                     break;
 278                 }
 279                 if (l.length() > 1 && l.charAt(0) == '+') {
 280                     String pkg = l.substring(1);
 281                     lastPackageSet = new HashSet<>();
 282                     rc.packageDependencies.put(pkg, lastPackageSet);
 283                 } else if (l.length() > 1 && lastPackageSet != null) {
 284                     lastPackageSet.add(l.substring(1));
 285                 }
 286             }
 287             // Load package pubapis
 288             Map<String, StringBuffer> tmp = new HashMap<>();
 289             StringBuffer lastPublicApi = null;
 290             for (;;) {
 291                 String l = in.readLine();
 292                 if (l == null) {
 293                     return new CompilationResult(ERROR_FATAL);
 294                 }
 295                 if (l.equals(JavacServer.PROTOCOL_SYSINFO)) {
 296                     break;
 297                 }
 298                 if (l.length() > 1 && l.charAt(0) == '+') {
 299                     String pkg = l.substring(1);
 300                     lastPublicApi = new StringBuffer();
 301                     tmp.put(pkg, lastPublicApi);
 302                 } else if (l.length() > 1 && lastPublicApi != null) {
 303                     lastPublicApi.append(l.substring(1));
 304                     lastPublicApi.append("\n");
 305                 }
 306             }
 307             for (String p : tmp.keySet()) {
 308                 //assert (packagePublicApis.get(p) == null);
 309                 String api = tmp.get(p).toString();
 310                 rc.packagePubapis.put(p, api);
 311             }
 312             // Now reading the max memory possible.
 313             for (;;) {
 314                 String l = in.readLine();
 315                 if (l == null) {
 316                     return new CompilationResult(ERROR_FATAL);
 317                 }
 318                 if (l.equals(JavacServer.PROTOCOL_RETURN_CODE)) {
 319                     break;
 320                 }
 321                 if (l.startsWith("num_cores=")) {
 322                     rc.sysinfo.numCores = Integer.parseInt(l.substring(10));
 323                 }
 324                 if (l.startsWith("max_memory=")) {
 325                     rc.sysinfo.maxMemory = Long.parseLong(l.substring(11));
 326                 }
 327             }
 328             String l = in.readLine();
 329             if (l == null) {
 330                 rc.setReturnCode(ERROR_FATAL);
 331                 rc.stderr = "No return value from the server!";
 332                 return rc;
 333             }
 334             rc.setReturnCode(Integer.parseInt(l));
 335             rc.stdout = stdout.toString();
 336             rc.stderr = stderr.toString();
 337         } catch (Exception e) {
 338             StringWriter sw = new StringWriter();
 339             e.printStackTrace(new PrintWriter(sw));
 340             rc.stderr = sw.toString();
 341         }
 342         return rc;
 343     }
 344 
 345     /**
 346      * Dispatch a compilation request to a javac server.
 347      *
 348      * @param args are the command line args to javac and is allowed to contain source files, @file and other command line options to javac.
 349      *
 350      * The generated classes, h files and other artifacts from the javac invocation are stored by the javac server to disk.
 351      *
 352      * @param sources_to_compile The sources to compile.
 353      *
 354      * @param visibleSources If visible sources has a non zero size, then visible_sources are the only files in the file system that the javac server can see!
 355      * (Sources to compile are always visible.) The visible sources are those supplied by the (filtered) -sourcepath
 356      *
 357      * @param visibleClasses If visible classes for a specific root/jar has a non zero size, then visible_classes are the only class files that the javac server
 358      * can see, in that root/jar. It maps from a classpath root or a jar file to the set of visible classes for that root/jar.
 359      *
 360      * The server return meta data about the build in the following parameters.
 361      * @param package_artifacts, map from package name to set of created artifacts for that package.
 362      * @param package_dependencies, map from package name to set of packages that it depends upon.
 363      * @param package_pubapis, map from package name to unique string identifying its pub api.
 364      */
 365     public CompilationResult useServer(String[] args,
 366                                        Set<URI> sourcesToCompile,
 367                                        Set<URI> visibleSources,
 368                                        Map<URI, Set<String>> visibleClasses) {
 369         try {
 370             if (portfile == null) {
 371                 CompilationResult cr = new CompilationResult(CompilationResult.ERROR_FATAL);
 372                 cr.stderr = "No portfile was specified!";
 373                 return cr;
 374             }
 375 
 376             int attempts = 0;
 377             CompilationResult rc;
 378             do {
 379                 PortFile port_file = JavacServer.getPortFile(portfile);
 380                 synchronized (port_file) {
 381                     port_file.lock();
 382                     port_file.getValues();
 383                     port_file.unlock();
 384                 }
 385                 if (!port_file.containsPortInfo()) {
 386                     String cmd = JavacServer.fork(sjavac, port_file.getFilename(), logfile, poolsize, keepalive, System.err, stdouterrfile, background);
 387 
 388                     if (background && !port_file.waitForValidValues()) {
 389                         // Ouch the server did not start! Lets print its stdouterrfile and the command used.
 390                         StringWriter sw = new StringWriter();
 391                         JavacServiceClient.printFailedAttempt(cmd, stdouterrfile, new PrintWriter(sw));
 392                         // And give up.
 393                         CompilationResult cr = new CompilationResult(ERROR_FATAL);
 394                         cr.stderr = sw.toString();
 395                         return cr;
 396                     }
 397                 }
 398                 rc = compileHelper(id, args, sourcesToCompile, visibleSources);
 399                 // Try again until we manage to connect. Any error after that
 400                 // will cause the compilation to fail.
 401                 if (rc.returnCode == CompilationResult.ERROR_BUT_TRY_AGAIN) {
 402                     // We could not connect to the server. Try again.
 403                     attempts++;
 404                     try {
 405                         Thread.sleep(JavacServer.WAIT_BETWEEN_CONNECT_ATTEMPTS * 1000);
 406                     } catch (InterruptedException e) {
 407                     }
 408                 }
 409             } while (rc.returnCode == ERROR_BUT_TRY_AGAIN && attempts < JavacServer.MAX_NUM_CONNECT_ATTEMPTS);
 410             return rc;
 411         } catch (Exception e) {
 412             StringWriter sw = new StringWriter();
 413             e.printStackTrace(new PrintWriter(sw));
 414             CompilationResult cr = new CompilationResult(ERROR_FATAL);
 415             cr.stderr = sw.toString();
 416             return cr;
 417         }
 418     }
 419 
 420     public static void printFailedAttempt(String cmd, String f, PrintWriter err) {
 421         err.println("---- Failed to start javac server with this command -----");
 422         err.println(cmd);
 423         try {
 424             BufferedReader in = new BufferedReader(new FileReader(f));
 425             err.println("---- stdout/stderr output from attempt to start javac server -----");
 426             for (;;) {
 427                 String l = in.readLine();
 428                 if (l == null) {
 429                     break;
 430                 }
 431                 err.println(l);
 432             }
 433             err.println("------------------------------------------------------------------");
 434         } catch (Exception e) {
 435             err.println("The stdout/stderr output in file " + f + " does not exist and the server did not start.");
 436         }
 437     }
 438 
 439     /**
 440      * Expect this key on the next line read from the reader.
 441      */
 442     public static boolean expect(BufferedReader in, String key) throws IOException {
 443         String s = in.readLine();
 444         if (s != null && s.equals(key)) {
 445             return true;
 446         }
 447         return false;
 448     }
 449 }