1 /*
   2  * Copyright (c) 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 com.sun.tools.javac.launcher;
  27 
  28 import java.io.BufferedInputStream;
  29 import java.io.BufferedReader;
  30 import java.io.ByteArrayOutputStream;
  31 import java.io.FilterOutputStream;
  32 import java.io.IOException;
  33 import java.io.InputStreamReader;
  34 import java.io.OutputStream;
  35 import java.io.OutputStreamWriter;
  36 import java.io.PrintStream;
  37 import java.io.PrintWriter;
  38 import java.lang.reflect.InvocationTargetException;
  39 import java.lang.reflect.Method;
  40 import java.lang.reflect.Modifier;
  41 import java.net.URI;
  42 import java.nio.charset.Charset;
  43 import java.nio.file.Files;
  44 import java.nio.file.InvalidPathException;
  45 import java.nio.file.Path;
  46 import java.nio.file.Paths;
  47 import java.text.MessageFormat;
  48 import java.util.ArrayList;
  49 import java.util.Arrays;
  50 import java.util.HashMap;
  51 import java.util.List;
  52 import java.util.Map;
  53 import java.util.MissingResourceException;
  54 import java.util.ResourceBundle;
  55 
  56 import javax.lang.model.element.NestingKind;
  57 import javax.lang.model.element.TypeElement;
  58 import javax.tools.FileObject;
  59 import javax.tools.ForwardingJavaFileManager;
  60 import javax.tools.JavaFileManager;
  61 import javax.tools.JavaFileManager.Location;
  62 import javax.tools.JavaFileObject;
  63 import javax.tools.SimpleJavaFileObject;
  64 import javax.tools.StandardJavaFileManager;
  65 import javax.tools.StandardLocation;
  66 
  67 import com.sun.source.util.JavacTask;
  68 import com.sun.source.util.TaskEvent;
  69 import com.sun.source.util.TaskListener;
  70 import com.sun.tools.javac.api.JavacTool;
  71 import com.sun.tools.javac.code.Source;
  72 import com.sun.tools.javac.resources.LauncherProperties.Errors;
  73 import com.sun.tools.javac.util.JCDiagnostic.Error;
  74 
  75 import jdk.internal.misc.VM;
  76 
  77 import static javax.tools.JavaFileObject.Kind.SOURCE;
  78 
  79 /**
  80  * Compiles a source file, and executes the main method it contains.
  81  *
  82  * <p><b>This is NOT part of any supported API.
  83  * If you write code that depends on this, you do so at your own
  84  * risk.  This code and its internal interfaces are subject to change
  85  * or deletion without notice.</b></p>
  86  */
  87 public class Main {
  88     /**
  89      * An exception used to report errors.
  90      */
  91     public class Fault extends Exception {
  92         private static final long serialVersionUID = 1L;
  93         Fault(Error error) {
  94             super(Main.this.getMessage(error));
  95         }
  96     }
  97 
  98     /**
  99      * Compiles a source file, and executes the main method it contains.
 100      *
 101      * <p>This is normally invoked from the Java launcher, either when
 102      * the {@code --source} option is used, or when the first argument
 103      * that is not part of a runtime option ends in {@code .java}.
 104      *
 105      * <p>The first entry in the {@code args} array is the source file
 106      * to be compiled and run; all subsequent entries are passed as
 107      * arguments to the main method of the first class found in the file.
 108      *
 109      * <p>If any problem occurs before executing the main class, it will
 110      * be reported to the standard error stream, and the the JVM will be
 111      * terminated by calling {@code System.exit} with a non-zero return code.
 112      *
 113      * @param args the arguments
 114      * @throws Throwable if the main method throws an exception
 115      */
 116     public static void main(String... args) throws Throwable {
 117         try {
 118             new Main(System.err).run(VM.getRuntimeArguments(), args);
 119         } catch (Fault f) {
 120             System.err.println(f.getMessage());
 121             System.exit(1);
 122         } catch (InvocationTargetException e) {
 123             // leave VM to handle the stacktrace, in the standard manner
 124             throw e.getTargetException();
 125         }
 126     }
 127 
 128     /** Stream for reporting errors, such as compilation errors. */
 129     private PrintWriter out;
 130 
 131     /**
 132      * Creates an instance of this class, providing a stream to which to report
 133      * any errors.
 134      *
 135      * @param out the stream
 136      */
 137     public Main(PrintStream out) {
 138         this(new PrintWriter(new OutputStreamWriter(out), true));
 139     }
 140 
 141     /**
 142      * Creates an instance of this class, providing a stream to which to report
 143      * any errors.
 144      *
 145      * @param out the stream
 146      */
 147     public Main(PrintWriter out) {
 148         this.out = out;
 149     }
 150 
 151     /**
 152      * Compiles a source file, and executes the main method it contains.
 153      *
 154      * <p>The first entry in the {@code args} array is the source file
 155      * to be compiled and run; all subsequent entries are passed as
 156      * arguments to the main method of the first class found in the file.
 157      *
 158      * <p>Options for {@code javac} are obtained by filtering the runtime arguments.
 159      *
 160      * <p>If the main method throws an exception, it will be propagated in an
 161      * {@code InvocationTargetException}. In that case, the stack trace of the
 162      * target exception will be truncated such that the main method will be the
 163      * last entry on the stack. In other words, the stack frames leading up to the
 164      * invocation of the main method will be removed.
 165      *
 166      * @param runtimeArgs the runtime arguments
 167      * @param args the arguments
 168      * @throws Fault if a problem is detected before the main method can be executed
 169      * @throws InvocationTargetException if the main method throws an exception
 170      */
 171     public void run(String[] runtimeArgs, String[] args) throws Fault, InvocationTargetException {
 172         Path file = getFile(args);
 173 
 174         Map<String, byte[]> inMemoryClasses = new HashMap<>();
 175         String mainClassName = compile(file, getJavacOpts(runtimeArgs), inMemoryClasses);
 176 
 177         String[] appArgs = Arrays.copyOfRange(args, 1, args.length);
 178         execute(mainClassName, appArgs, inMemoryClasses);
 179     }
 180 
 181     /**
 182      * Returns the path for the filename found in the first of an array of arguments.
 183      *
 184      * @param args the array
 185      * @return the path
 186      * @throws Fault if there is a problem determining the path, or if the file does not exist
 187      */
 188     private Path getFile(String[] args) throws Fault {
 189         if (args.length == 0) {
 190             // should not happen when invoked from launcher
 191             throw new Fault(Errors.NoArgs);
 192         }
 193         Path file;
 194         try {
 195             file = Paths.get(args[0]);
 196         } catch (InvalidPathException e) {
 197             throw new Fault(Errors.InvalidFilename(args[0]));
 198         }
 199         if (!Files.exists(file)) {
 200             // should not happen when invoked from launcher
 201             throw new Fault(Errors.FileNotFound(file));
 202         }
 203         return file;
 204     }
 205 
 206     /**
 207      * Reads a source file, ignoring the first line if it begins {@code #!}.
 208      *
 209      * <p>If the first two bytes are {@code #!}, indicating a "magic number" of an executable
 210      * text file, the rest of the first line up to but not including the newline is ignored.
 211      * All characters after the first two are read in the
 212      * {@link Charset#defaultCharset default platform encoding}.
 213      *
 214      * @param file the file
 215      * @return a file object containing the content of the file
 216      * @throws Fault if an error occurs while reading the file
 217      */
 218     private JavaFileObject readFile(Path file) throws Fault {
 219         // use a BufferedInputStream to guarantee that we can use mark and reset.
 220         try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(file))) {
 221             in.mark(2);
 222             int magic = (in.read() << 8) + in.read();
 223             boolean shebang = magic == (('#' << 8) + '!');
 224             if (!shebang) {
 225                 in.reset();
 226             }
 227             try (BufferedReader r = new BufferedReader(new InputStreamReader(in, Charset.defaultCharset()))) {
 228                 StringBuilder sb = new StringBuilder();
 229                 if (shebang) {
 230                     r.readLine();
 231                     sb.append("\n"); // preserve line numbers
 232                 }
 233                 char[] buf = new char[1024];
 234                 int n;
 235                 while ((n = r.read(buf, 0, buf.length)) != -1)  {
 236                     sb.append(buf, 0, n);
 237                 }
 238                 return new SimpleJavaFileObject(file.toUri(), SOURCE) {
 239                     @Override
 240                     public String getName() {
 241                         return file.toString();
 242                     }
 243                     @Override
 244                     public CharSequence getCharContent(boolean ignoreEncodingErrors) {
 245                         return sb;
 246                     }
 247                     @Override
 248                     public boolean isNameCompatible(String simpleName, JavaFileObject.Kind kind) {
 249                         return (kind == JavaFileObject.Kind.SOURCE)
 250                                 && !simpleName.equals("package-info");
 251                     }
 252                     @Override
 253                     public String toString() {
 254                         return "JavacSourceLauncher[" + file + "]";
 255                     }
 256                 };
 257             }
 258         } catch (IOException e) {
 259             throw new Fault(Errors.CantReadFile(file, e));
 260         }
 261     }
 262 
 263     /**
 264      * Returns the subset of the runtime arguments that are relevant to {@code javac}.
 265      * Generally, the relevant options are those for setting paths and for configuring the
 266      * module system.
 267      *
 268      * @param runtimeArgs the runtime arguments
 269      * @return the subset of the runtime arguments
 270      **/
 271     private List<String> getJavacOpts(String... runtimeArgs) throws Fault {
 272         List<String> javacOpts = new ArrayList<>();
 273 
 274         String sourceOpt = System.getProperty("jdk.internal.javac.source");
 275         if (sourceOpt != null) {
 276             Source source = Source.lookup(sourceOpt);
 277             if (source == null) {
 278                 throw new Fault(Errors.InvalidValueForSource(sourceOpt));
 279             }
 280             javacOpts.addAll(List.of("--release", sourceOpt));
 281         }
 282 
 283         for (int i = 0; i < runtimeArgs.length; i++) {
 284             String arg = runtimeArgs[i];
 285             String opt = arg, value = null;
 286             if (arg.startsWith("--")) {
 287                 int eq = arg.indexOf('=');
 288                 if (eq > 0) {
 289                     opt = arg.substring(0, eq);
 290                     value = arg.substring(eq + 1);
 291                 }
 292             }
 293             switch (opt) {
 294                 // The following options all expect a value, either in the following
 295                 // position, or after '=', for options beginning "--".
 296                 case "--class-path": case "-classpath": case "-cp":
 297                 case "--module-path": case "-p":
 298                 case "--add-exports":
 299                 case "--add-modules":
 300                 case "--limit-modules":
 301                 case "--patch-module":
 302                 case "--upgrade-module-path":
 303                     if (value == null) {
 304                         if (i== runtimeArgs.length - 1) {
 305                             // should not happen when invoked from launcher
 306                             throw new Fault(Errors.NoValueForOption(opt));
 307                         }
 308                         value = runtimeArgs[++i];
 309                     }
 310                     if (opt.equals("--add-modules") && value.equals("ALL-DEFAULT")) {
 311                         break;
 312                     }
 313                     javacOpts.add(opt);
 314                     javacOpts.add(value);
 315                     break;
 316                 default:
 317                     // ignore all other runtime args
 318             }
 319         }
 320         return javacOpts;
 321     }
 322 
 323     /**
 324      * Compiles a source file, placing the class files in a map in memory.
 325      * Any messages generated during compilation will be written to the stream
 326      * provided when this object was created.
 327      *
 328      * @param file the source file
 329      * @param javacOptscompilation options for {@code javac}
 330      * @param inMemoryClasses a map in which to store the generated classes
 331      * @return the name of the first class found in the source file
 332      * @throws Fault if any compilation errors occur, or if no class was found
 333      */
 334     private String compile(Path file, List<String> javacOpts, Map<String, byte[]> inMemoryClasses) throws Fault {
 335         JavaFileObject fo = readFile(file);
 336 
 337         JavacTool javaCompiler = JavacTool.create();
 338         StandardJavaFileManager stdFileMgr = javaCompiler.getStandardFileManager(null, null, null);
 339         JavaFileManager fm = new MemoryFileManager(inMemoryClasses, stdFileMgr);
 340         JavacTask t = javaCompiler.getTask(out, fm, null, javacOpts, null, List.of(fo));
 341         MainClassListener l = new MainClassListener(t);
 342         Boolean ok = t.call();
 343         if (!ok) {
 344             throw new Fault(Errors.CompilationFailed);
 345         }
 346         if (l.mainClass == null) {
 347             throw new Fault(Errors.NoClass);
 348         }
 349         String mainClassName = l.mainClass.getQualifiedName().toString();
 350         return mainClassName;
 351     }
 352 
 353     /**
 354      * Invokes the {@code main} method of a specified class, using a class loader that
 355      * will load recently compiled classes from memory.
 356      *
 357      * @param mainClassName the class to be executed
 358      * @param appArgs the arguments for the {@code main} method
 359      * @param inMemoryClasses the map of recently compiled classes
 360      * @throws Fault if there is a problem finding or invoking the {@code main} method
 361      * @throws InvocationTargetException if the {@code main} method throws an exception
 362      */
 363     private void execute(String mainClassName, String[] appArgs, Map<String, byte[]> inMemoryClasses)
 364             throws Fault, InvocationTargetException {
 365         ClassLoader cl = new MemoryClassLoader(inMemoryClasses, ClassLoader.getSystemClassLoader());
 366         try {
 367             Class<?> appClass = Class.forName(mainClassName, true, cl);
 368             if (appClass.getClassLoader() != cl) {
 369                 throw new Fault(Errors.UnexpectedClass(mainClassName));
 370             }
 371             Method main = appClass.getDeclaredMethod("main", String[].class);
 372             int PUBLIC_STATIC = Modifier.PUBLIC | Modifier.STATIC;
 373             if ((main.getModifiers() & PUBLIC_STATIC) != PUBLIC_STATIC) {
 374                 throw new Fault(Errors.MainNotPublicStatic);
 375             }
 376             if (!main.getReturnType().equals(void.class)) {
 377                 throw new Fault(Errors.MainNotVoid);
 378             }
 379             main.setAccessible(true);
 380             main.invoke(0, (Object) appArgs);
 381         } catch (ClassNotFoundException e) {
 382             throw new Fault(Errors.CantFindClass(mainClassName));
 383         } catch (NoSuchMethodException e) {
 384             throw new Fault(Errors.CantFindMainMethod(mainClassName));
 385         } catch (IllegalAccessException e) {
 386             throw new Fault(Errors.CantAccessMainMethod(mainClassName));
 387         } catch (InvocationTargetException e) {
 388             // remove stack frames for source launcher
 389             int invocationFrames = e.getStackTrace().length;
 390             Throwable target = e.getTargetException();
 391             StackTraceElement[] targetTrace = target.getStackTrace();
 392             target.setStackTrace(Arrays.copyOfRange(targetTrace, 0, targetTrace.length - invocationFrames));
 393             throw e;
 394         }
 395     }
 396 
 397     private static final String bundleName = "com.sun.tools.javac.resources.launcher";
 398     private ResourceBundle resourceBundle = null;
 399     private String errorPrefix;
 400 
 401     /**
 402      * Returns a localized string from a resource bundle.
 403      *
 404      * @param key the key for the resource
 405      * @param args values to be inserted into the resource
 406      * @return the localized string
 407      */
 408     private String getMessage(Error error) {
 409         String key = error.key();
 410         Object[] args = error.getArgs();
 411         try {
 412             if (resourceBundle == null) {
 413                 resourceBundle = ResourceBundle.getBundle(bundleName);
 414                 errorPrefix = resourceBundle.getString("launcher.error");
 415             }
 416             String resource = resourceBundle.getString(key);
 417             String message = MessageFormat.format(resource, args);
 418             return errorPrefix + message;
 419         } catch (MissingResourceException e) {
 420             return "Cannot access resource; " + key + Arrays.toString(args);
 421         }
 422     }
 423 
 424     /**
 425      * A listener to detect the first class found in a compilation.
 426      */
 427     static class MainClassListener implements TaskListener {
 428         private final JavacTask task;
 429         TypeElement mainClass;
 430 
 431         MainClassListener(JavacTask t) {
 432             task = t;
 433             t.addTaskListener(this);
 434         }
 435 
 436         @Override
 437         public void started(TaskEvent ev) {
 438             if (ev.getKind() == TaskEvent.Kind.ANALYZE && mainClass == null) {
 439                 TypeElement te = ev.getTypeElement();
 440                 if (te.getNestingKind() == NestingKind.TOP_LEVEL) {
 441                     mainClass = te;
 442                 }
 443             }
 444         }
 445     }
 446 
 447     /**
 448      * An in-memory file manager.
 449      *
 450      * <p>Class files (of kind {@JavaFileObject.Kind.CLASS CLASS} written to
 451      * {@link StandardLocation#CLASS_OUTPUT} will be written to an in-memory cache.
 452      * All other file manager operations will be delegated to a specified file manager.
 453      */
 454     static class MemoryFileManager extends ForwardingJavaFileManager<JavaFileManager> {
 455         private final Map<String, byte[]> map;
 456 
 457         MemoryFileManager(Map<String, byte[]> map, JavaFileManager delegate) {
 458             super(delegate);
 459             this.map = map;
 460         }
 461 
 462         @Override
 463         public JavaFileObject getJavaFileForOutput(Location location, String className,
 464                 JavaFileObject.Kind kind, FileObject sibling) throws IOException {
 465             if (location == StandardLocation.CLASS_OUTPUT && kind == JavaFileObject.Kind.CLASS) {
 466                 return createInMemoryClassFile(className);
 467             } else {
 468                 return super.getJavaFileForOutput(location, className, kind, sibling);
 469             }
 470         }
 471 
 472         private JavaFileObject createInMemoryClassFile(String className) {
 473             URI uri = URI.create("memory:///" + className.replace('.', '/') + ".class");
 474             return new SimpleJavaFileObject(uri, JavaFileObject.Kind.CLASS) {
 475                 @Override
 476                 public OutputStream openOutputStream() {
 477                     return new FilterOutputStream(new ByteArrayOutputStream()) {
 478                         @Override
 479                         public void close() throws IOException {
 480                             out.close();
 481                             byte[] bytes = ((ByteArrayOutputStream) out).toByteArray();
 482                             map.put(className, bytes);
 483                         }
 484                     };
 485                 }
 486             };
 487         }
 488     }
 489 
 490     /**
 491      * An in-memory classloader, that uses an in-memory cache written by {@link MemoryFileManager}.
 492      *
 493      * <p>The classloader uses the standard parent-delegation model, just providing
 494      * {@code findClass} to find classes in the in-memory cache.
 495      */
 496     static class MemoryClassLoader extends ClassLoader {
 497         private final Map<String, byte[]> map;
 498 
 499         MemoryClassLoader(Map<String, byte[]> map, ClassLoader parent) {
 500             super(parent);
 501             this.map = map;
 502         }
 503 
 504         @Override
 505         protected Class<?> findClass(String name) throws ClassNotFoundException {
 506             byte[] bytes = map.get(name);
 507             if (bytes == null) {
 508                 throw new ClassNotFoundException(name);
 509             }
 510             return defineClass(name, bytes, 0, bytes.length);
 511         }
 512     }
 513 }