| // Copyright (C) 2009 The Android Open Source Project | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package com.google.gerrit.launcher; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import static java.util.concurrent.TimeUnit.DAYS; | |
| import static java.util.concurrent.TimeUnit.MILLISECONDS; | |
| import java.io.File; | |
| import java.io.FileNotFoundException; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.OutputStream; | |
| import java.lang.reflect.InvocationTargetException; | |
| import java.lang.reflect.Method; | |
| import java.lang.reflect.Modifier; | |
| import java.net.JarURLConnection; | |
| import java.net.MalformedURLException; | |
| import java.net.URI; | |
| import java.net.URL; | |
| import java.net.URLClassLoader; | |
| import java.nio.file.FileSystem; | |
| import java.nio.file.FileSystems; | |
| import java.nio.file.Files; | |
| import java.nio.file.NoSuchFileException; | |
| import java.nio.file.Path; | |
| import java.nio.file.Paths; | |
| import java.security.CodeSource; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.Collections; | |
| import java.util.HashMap; | |
| import java.util.Iterator; | |
| import java.util.List; | |
| import java.util.Locale; | |
| import java.util.Map; | |
| import java.util.NavigableMap; | |
| import java.util.Properties; | |
| import java.util.TreeMap; | |
| import java.util.jar.Attributes; | |
| import java.util.jar.JarFile; | |
| import java.util.jar.Manifest; | |
| import java.util.zip.ZipEntry; | |
| import java.util.zip.ZipFile; | |
| /** Main class for a JAR file to run code from "WEB-INF/lib". */ | |
| public final class GerritLauncher { | |
| private static final String PKG = "com.google.gerrit.pgm"; | |
| public static final String NOT_ARCHIVED = "NOT_ARCHIVED"; | |
| // Classloader that allows to add additional jars to the classpath. | |
| public static class GerritClassLoader extends URLClassLoader { | |
| static { | |
| ClassLoader.registerAsParallelCapable(); | |
| } | |
| /** | |
| * Constructs a new URLClassLoader for the given URLs. | |
| * | |
| * @param urls the URLs from which to load classes and resources | |
| * @param parent the parent class loader for delegation | |
| */ | |
| GerritClassLoader(URL[] urls, ClassLoader parent) { | |
| super(urls, parent); | |
| } | |
| /** | |
| * Appends the additional URL to the list of URLs to search for classes and resources. | |
| * | |
| * @param url the URL to be added to the search path of URLs | |
| */ | |
| @Override | |
| public void addURL(URL url) { | |
| super.addURL(url); | |
| } | |
| } | |
| private static ClassLoader daemonClassLoader; | |
| public static void main(String[] argv) throws Exception { | |
| System.exit(mainImpl(argv)); | |
| } | |
| /** | |
| * Invokes a program. | |
| * | |
| * <p>Creates a new classloader to load and run the program class. To reuse a classloader across | |
| * calls (e.g. from tests), use {@link #invokeProgram(ClassLoader, String[])}. | |
| * | |
| * @param argv arguments, as would be passed to {@code gerrit.war}. The first argument is the | |
| * program name. | |
| * @return program return code. | |
| * @throws Exception if any error occurs. | |
| */ | |
| public static int mainImpl(String[] argv) throws Exception { | |
| if (argv.length == 0 || "-h".equals(argv[0]) || "--help".equals(argv[0])) { | |
| File me; | |
| try { | |
| me = getDistributionArchive(); | |
| } catch (FileNotFoundException e) { | |
| me = null; | |
| } | |
| String jar = me != null ? me.getName() : "gerrit.war"; | |
| System.err.println("Gerrit Code Review " + getVersion(me)); | |
| System.err.println("usage: java -jar " + jar + " command [ARG ...]"); | |
| System.err.println(); | |
| System.err.println("The most commonly used commands are:"); | |
| System.err.println(" init Initialize a Gerrit installation"); | |
| System.err.println(" reindex Rebuild the secondary index"); | |
| System.err.println(" daemon Run the Gerrit network daemons"); | |
| System.err.println(" version Display the build version number"); | |
| System.err.println(" passwd Set or change password in secure.config"); | |
| System.err.println(); | |
| System.err.println(" ls List files available for cat"); | |
| System.err.println(" cat FILE Display a file from the archive"); | |
| System.err.println(); | |
| return 1; | |
| } | |
| // Special cases, a few global options actually are programs. | |
| // | |
| if ("-v".equals(argv[0]) || "--version".equals(argv[0])) { | |
| argv[0] = "version"; | |
| } else if ("-p".equals(argv[0]) || "--cat".equals(argv[0])) { | |
| argv[0] = "cat"; | |
| } else if ("-l".equals(argv[0]) || "--ls".equals(argv[0])) { | |
| argv[0] = "ls"; | |
| } | |
| // Run the application class | |
| // | |
| final ClassLoader cl = libClassLoader(isProlog(programClassName(argv[0]))); | |
| Thread.currentThread().setContextClassLoader(cl); | |
| return invokeProgram(cl, argv); | |
| } | |
| public static void daemonStart(String[] argv) throws Exception { | |
| if (daemonClassLoader != null) { | |
| throw new IllegalStateException("daemonStart can be called only once per JVM instance"); | |
| } | |
| final ClassLoader cl = libClassLoader(false); | |
| Thread.currentThread().setContextClassLoader(cl); | |
| daemonClassLoader = cl; | |
| String[] daemonArgv = new String[argv.length + 1]; | |
| daemonArgv[0] = "daemon"; | |
| for (int i = 0; i < argv.length; i++) { | |
| daemonArgv[i + 1] = argv[i]; | |
| } | |
| int res = invokeProgram(cl, daemonArgv); | |
| if (res != 0) { | |
| throw new Exception("Unexpected return value: " + res); | |
| } | |
| } | |
| public static void daemonStop(String[] argv) throws Exception { | |
| if (daemonClassLoader == null) { | |
| throw new IllegalStateException("daemonStop can be called only after call to daemonStop"); | |
| } | |
| String[] daemonArgv = new String[argv.length + 2]; | |
| daemonArgv[0] = "daemon"; | |
| daemonArgv[1] = "--stop-only"; | |
| for (int i = 0; i < argv.length; i++) { | |
| daemonArgv[i + 2] = argv[i]; | |
| } | |
| int res = invokeProgram(daemonClassLoader, daemonArgv); | |
| if (res != 0) { | |
| throw new Exception("Unexpected return value: " + res); | |
| } | |
| } | |
| private static boolean isProlog(String cn) { | |
| return "PrologShell".equals(cn) || "Rulec".equals(cn); | |
| } | |
| private static String getVersion(File me) { | |
| if (me == null) { | |
| return ""; | |
| } | |
| try (JarFile jar = new JarFile(me)) { | |
| Manifest mf = jar.getManifest(); | |
| Attributes att = mf.getMainAttributes(); | |
| String val = att.getValue(Attributes.Name.IMPLEMENTATION_VERSION); | |
| return val != null ? val : ""; | |
| } catch (IOException e) { | |
| return ""; | |
| } | |
| } | |
| /** | |
| * Invokes a program in the provided {@code ClassLoader}. | |
| * | |
| * @param loader classloader to load program class from. | |
| * @param origArgv arguments, as would be passed to {@code gerrit.war}. The first argument is the | |
| * program name. | |
| * @return program return code. | |
| * @throws Exception if any error occurs. | |
| */ | |
| public static int invokeProgram(ClassLoader loader, String[] origArgv) throws Exception { | |
| String name = origArgv[0]; | |
| final String[] argv = new String[origArgv.length - 1]; | |
| System.arraycopy(origArgv, 1, argv, 0, argv.length); | |
| Class<?> clazz; | |
| try { | |
| try { | |
| String cn = programClassName(name); | |
| clazz = Class.forName(PKG + "." + cn, true, loader); | |
| } catch (ClassNotFoundException cnfe) { | |
| if (name.equals(name.toLowerCase(Locale.US))) { | |
| clazz = Class.forName(PKG + "." + name, true, loader); | |
| } else { | |
| throw cnfe; | |
| } | |
| } | |
| } catch (ClassNotFoundException cnfe) { | |
| System.err.println("fatal: unknown command " + name); | |
| System.err.println(" (no " + PKG + "." + name + ")"); | |
| return 1; | |
| } | |
| final Method main; | |
| try { | |
| main = clazz.getMethod("main", argv.getClass()); | |
| } catch (SecurityException | NoSuchMethodException e) { | |
| System.err.println("fatal: unknown command " + name); | |
| return 1; | |
| } | |
| final Object res; | |
| try { | |
| if ((main.getModifiers() & Modifier.STATIC) == Modifier.STATIC) { | |
| res = main.invoke(null, new Object[] {argv}); | |
| } else { | |
| res = | |
| main.invoke(clazz.getConstructor(new Class<?>[] {}).newInstance(), new Object[] {argv}); | |
| } | |
| } catch (InvocationTargetException ite) { | |
| if (ite.getCause() instanceof Exception) { | |
| throw (Exception) ite.getCause(); | |
| } else if (ite.getCause() instanceof Error) { | |
| throw (Error) ite.getCause(); | |
| } else { | |
| throw ite; | |
| } | |
| } | |
| if (res instanceof Number) { | |
| return ((Number) res).intValue(); | |
| } | |
| return 0; | |
| } | |
| private static String programClassName(String cn) { | |
| if (cn.equals(cn.toLowerCase(Locale.US))) { | |
| StringBuilder buf = new StringBuilder(); | |
| buf.append(Character.toUpperCase(cn.charAt(0))); | |
| for (int i = 1; i < cn.length(); i++) { | |
| if (cn.charAt(i) == '-' && i + 1 < cn.length()) { | |
| i++; | |
| buf.append(Character.toUpperCase(cn.charAt(i))); | |
| } else { | |
| buf.append(cn.charAt(i)); | |
| } | |
| } | |
| return buf.toString(); | |
| } | |
| return cn; | |
| } | |
| private static ClassLoader libClassLoader(boolean prologCompiler) throws IOException { | |
| final File path; | |
| try { | |
| path = getDistributionArchive(); | |
| } catch (FileNotFoundException e) { | |
| if (NOT_ARCHIVED.equals(e.getMessage())) { | |
| return useDevClasspath(); | |
| } | |
| throw e; | |
| } | |
| final NavigableMap<String, URL> jars = new TreeMap<>(); | |
| try (ZipFile zf = new ZipFile(path)) { | |
| Iterator<? extends ZipEntry> zipEntryIt = zf.stream().iterator(); | |
| while (zipEntryIt.hasNext()) { | |
| final ZipEntry ze = zipEntryIt.next(); | |
| if (ze.isDirectory()) { | |
| continue; | |
| } | |
| String name = ze.getName(); | |
| if (name.startsWith("WEB-INF/lib/")) { | |
| extractJar(zf, ze, jars); | |
| } else if (name.startsWith("WEB-INF/pgm-lib/")) { | |
| // Some Prolog tools are restricted. | |
| if (prologCompiler || !name.startsWith("WEB-INF/pgm-lib/prolog-")) { | |
| extractJar(zf, ze, jars); | |
| } | |
| } | |
| } | |
| } catch (IOException e) { | |
| throw new IOException("Cannot obtain libraries from " + path, e); | |
| } | |
| if (jars.isEmpty()) { | |
| return GerritLauncher.class.getClassLoader(); | |
| } | |
| // The extension API needs to be its own ClassLoader, along | |
| // with a few of its dependencies. Try to construct this first. | |
| List<URL> extapi = new ArrayList<>(); | |
| move(jars, "gerrit-extension-api-", extapi); | |
| move(jars, "guice-", extapi); | |
| move(jars, "javax.inject-1.jar", extapi); | |
| move(jars, "aopalliance-1.0.jar", extapi); | |
| move(jars, "guice-servlet-", extapi); | |
| move(jars, "servlet-api-", extapi); | |
| ClassLoader parent = ClassLoader.getSystemClassLoader(); | |
| if (!extapi.isEmpty()) { | |
| parent = URLClassLoader.newInstance(extapi.toArray(new URL[extapi.size()]), parent); | |
| } | |
| return new GerritClassLoader(jars.values().toArray(new URL[jars.size()]), parent); | |
| } | |
| private static void extractJar(ZipFile zf, ZipEntry ze, NavigableMap<String, URL> jars) | |
| throws IOException { | |
| File tmp = createTempFile(safeName(ze), ".jar"); | |
| try (OutputStream out = Files.newOutputStream(tmp.toPath()); | |
| InputStream in = zf.getInputStream(ze)) { | |
| byte[] buf = new byte[4096]; | |
| int n; | |
| while ((n = in.read(buf, 0, buf.length)) > 0) { | |
| out.write(buf, 0, n); | |
| } | |
| } | |
| String name = ze.getName(); | |
| jars.put(name.substring(name.lastIndexOf('/')), tmp.toURI().toURL()); | |
| } | |
| private static void move(NavigableMap<String, URL> jars, String prefix, List<URL> extapi) { | |
| NavigableMap<String, URL> matches = jars.tailMap(prefix, /* inclusive= */ true); | |
| if (!matches.isEmpty()) { | |
| String first = matches.firstKey(); | |
| if (first.startsWith(prefix)) { | |
| extapi.add(jars.remove(first)); | |
| } | |
| } | |
| } | |
| private static String safeName(ZipEntry ze) { | |
| // Try to derive the name of the temporary file so it | |
| // doesn't completely suck. Best if we can make it | |
| // match the name it was in the archive. | |
| // | |
| String name = ze.getName(); | |
| if (name.contains("/")) { | |
| name = name.substring(name.lastIndexOf('/') + 1); | |
| } | |
| if (name.contains(".")) { | |
| name = name.substring(0, name.lastIndexOf('.')); | |
| } | |
| if (name.isEmpty()) { | |
| name = "code"; | |
| } | |
| return name; | |
| } | |
| private static volatile File myArchive; | |
| private static volatile File myHome; | |
| private static final Map<Path, FileSystem> zipFileSystems = new HashMap<>(); | |
| /** | |
| * Locate the JAR/WAR file we were launched from. | |
| * | |
| * @return local path of the Gerrit WAR file. | |
| * @throws FileNotFoundException if the code cannot guess the location. | |
| */ | |
| public static File getDistributionArchive() throws FileNotFoundException, IOException { | |
| File result = myArchive; | |
| if (result == null) { | |
| synchronized (GerritLauncher.class) { | |
| result = myArchive; | |
| if (result != null) { | |
| return result; | |
| } | |
| result = locateMyArchive(); | |
| myArchive = result; | |
| } | |
| } | |
| return result; | |
| } | |
| public static synchronized FileSystem getZipFileSystem(Path zip) throws IOException { | |
| // FileSystems canonicalizes the path, so we should too. | |
| zip = zip.toRealPath(); | |
| FileSystem zipFs = zipFileSystems.get(zip); | |
| if (zipFs == null) { | |
| zipFs = newZipFileSystem(zip); | |
| zipFileSystems.put(zip, zipFs); | |
| } | |
| return zipFs; | |
| } | |
| public static FileSystem newZipFileSystem(Path zip) throws IOException { | |
| return FileSystems.newFileSystem( | |
| URI.create("jar:" + zip.toUri()), Collections.<String, String>emptyMap()); | |
| } | |
| private static File locateMyArchive() throws FileNotFoundException { | |
| final ClassLoader myCL = GerritLauncher.class.getClassLoader(); | |
| final String myName = GerritLauncher.class.getName().replace('.', '/') + ".class"; | |
| final URL myClazz = myCL.getResource(myName); | |
| if (myClazz == null) { | |
| throw new FileNotFoundException("Cannot find JAR: no " + myName); | |
| } | |
| // ZipFile may have the path of our JAR hiding within itself. | |
| // | |
| try { | |
| JarFile jar = ((JarURLConnection) myClazz.openConnection()).getJarFile(); | |
| File path = new File(jar.getName()); | |
| if (path.isFile()) { | |
| return path; | |
| } | |
| } catch (Exception e) { | |
| // Nope, that didn't work. Try a different method. | |
| // | |
| } | |
| // Maybe this is a local class file, running under a debugger? | |
| // | |
| if ("file".equals(myClazz.getProtocol())) { | |
| final File path = new File(myClazz.getPath()); | |
| if (path.isFile() && path.getParentFile().isDirectory()) { | |
| throw new FileNotFoundException(NOT_ARCHIVED); | |
| } | |
| } | |
| // The CodeSource might be able to give us the source as a stream. | |
| // If so, copy it to a local file so we have random access to it. | |
| // | |
| final CodeSource src = GerritLauncher.class.getProtectionDomain().getCodeSource(); | |
| if (src != null) { | |
| try (InputStream in = src.getLocation().openStream()) { | |
| final File tmp = createTempFile("gerrit_", ".zip"); | |
| try (OutputStream out = Files.newOutputStream(tmp.toPath())) { | |
| final byte[] buf = new byte[4096]; | |
| int n; | |
| while ((n = in.read(buf, 0, buf.length)) > 0) { | |
| out.write(buf, 0, n); | |
| } | |
| } | |
| return tmp; | |
| } catch (IOException e) { | |
| // Nope, that didn't work. | |
| // | |
| } | |
| } | |
| throw new FileNotFoundException("Cannot find local copy of JAR"); | |
| } | |
| private static boolean temporaryDirectoryFound; | |
| private static File temporaryDirectory; | |
| /** | |
| * Creates a temporary file within the application's unpack location. | |
| * | |
| * <p>The launcher unpacks the nested JAR files into a temporary directory, allowing the classes | |
| * to be loaded from local disk with standard Java APIs. This method constructs a new temporary | |
| * file in the same directory. | |
| * | |
| * <p>The method first tries to create {@code prefix + suffix} within the directory under the | |
| * assumption that a given {@code prefix + suffix} combination is made at most once per JVM | |
| * execution. If this fails (e.g. the named file already exists) a mangled unique name is used and | |
| * returned instead, with the unique string appearing between the prefix and suffix. | |
| * | |
| * <p>Files created by this method will be automatically deleted by the JVM when it terminates. If | |
| * the returned file is converted into a directory by the caller, the caller must arrange for the | |
| * contents to be deleted before the directory is. | |
| * | |
| * <p>If supported by the underlying operating system, the temporary directory which contains | |
| * these temporary files is accessible only by the user running the JVM. | |
| * | |
| * @param prefix prefix of the file name. | |
| * @param suffix suffix of the file name. | |
| * @return the path of the temporary file. The returned object exists in the filesystem as a file; | |
| * caller may need to delete and recreate as a directory if a directory was preferred. | |
| * @throws IOException the file could not be created. | |
| */ | |
| public static synchronized File createTempFile(String prefix, String suffix) throws IOException { | |
| if (!temporaryDirectoryFound) { | |
| final File d = File.createTempFile("gerrit_", "_app", tmproot()); | |
| if (d.delete() && d.mkdir()) { | |
| // Try to lock the directory down to be accessible by us. | |
| // We first have to remove all permissions, then add back | |
| // only the owner permissions. | |
| // | |
| d.setWritable(false, false /* all */); | |
| d.setReadable(false, false /* all */); | |
| d.setExecutable(false, false /* all */); | |
| d.setWritable(true, true /* owner only */); | |
| d.setReadable(true, true /* owner only */); | |
| d.setExecutable(true, true /* owner only */); | |
| d.deleteOnExit(); | |
| temporaryDirectory = d; | |
| } | |
| temporaryDirectoryFound = true; | |
| } | |
| if (temporaryDirectory != null) { | |
| // If we have a private directory and this name has not yet | |
| // been used within the private directory, create it as-is. | |
| // | |
| final File tmp = new File(temporaryDirectory, prefix + suffix); | |
| if (tmp.createNewFile()) { | |
| tmp.deleteOnExit(); | |
| return tmp; | |
| } | |
| } | |
| if (!prefix.endsWith("_")) { | |
| prefix += "_"; | |
| } | |
| final File tmp = File.createTempFile(prefix, suffix, temporaryDirectory); | |
| tmp.deleteOnExit(); | |
| return tmp; | |
| } | |
| /** | |
| * Provide path to a working directory | |
| * | |
| * @return local path of the working directory or null if cannot be determined | |
| */ | |
| public static File getHomeDirectory() { | |
| if (myHome == null) { | |
| myHome = locateHomeDirectory(); | |
| } | |
| return myHome; | |
| } | |
| @SuppressWarnings("ReturnMissingNullable") | |
| private static File tmproot() { | |
| File tmp; | |
| String gerritTemp = System.getenv("GERRIT_TMP"); | |
| if (gerritTemp != null && gerritTemp.length() > 0) { | |
| tmp = new File(gerritTemp); | |
| } else { | |
| tmp = new File(getHomeDirectory(), "tmp"); | |
| } | |
| if (!tmp.exists() && !tmp.mkdirs()) { | |
| System.err.println("warning: cannot create " + tmp.getAbsolutePath()); | |
| System.err.println("warning: using system temporary directory instead"); | |
| return null; | |
| } | |
| // Try to clean up any stale empty directories. Assume any empty | |
| // directory that is older than 7 days is one of these dead ones | |
| // that we can clean up. | |
| // | |
| final File[] tmpEntries = tmp.listFiles(); | |
| if (tmpEntries != null) { | |
| final long now = System.currentTimeMillis(); | |
| final long expired = now - MILLISECONDS.convert(7, DAYS); | |
| for (File tmpEntry : tmpEntries) { | |
| if (tmpEntry.isDirectory() && tmpEntry.lastModified() < expired) { | |
| final String[] all = tmpEntry.list(); | |
| if (all == null || all.length == 0) { | |
| tmpEntry.delete(); | |
| } | |
| } | |
| } | |
| } | |
| try { | |
| return tmp.getCanonicalFile(); | |
| } catch (IOException e) { | |
| return tmp; | |
| } | |
| } | |
| @SuppressWarnings("ReturnMissingNullable") | |
| private static File locateHomeDirectory() { | |
| // Try to find the user's home directory. If we can't find it | |
| // return null so the JVM's default temporary directory is used | |
| // instead. This is probably /tmp or /var/tmp. | |
| // | |
| String userHome = System.getProperty("user.home"); | |
| if (userHome == null || "".equals(userHome)) { | |
| userHome = System.getenv("HOME"); | |
| if (userHome == null || "".equals(userHome)) { | |
| System.err.println("warning: cannot determine home directory"); | |
| System.err.println("warning: using system temporary directory instead"); | |
| return null; | |
| } | |
| } | |
| // Ensure the home directory exists. If it doesn't, try to make it. | |
| // | |
| final File home = new File(userHome); | |
| if (!home.exists()) { | |
| if (home.mkdirs()) { | |
| System.err.println("warning: created " + home.getAbsolutePath()); | |
| } else { | |
| System.err.println("warning: " + home.getAbsolutePath() + " not found"); | |
| System.err.println("warning: using system temporary directory instead"); | |
| return null; | |
| } | |
| } | |
| // Use $HOME/.gerritcodereview/tmp for our temporary file area. | |
| // | |
| final File gerrithome = new File(home, ".gerritcodereview"); | |
| if (!gerrithome.exists() && !gerrithome.mkdirs()) { | |
| System.err.println("warning: cannot create " + gerrithome.getAbsolutePath()); | |
| System.err.println("warning: using system temporary directory instead"); | |
| return null; | |
| } | |
| try { | |
| return gerrithome.getCanonicalFile(); | |
| } catch (IOException e) { | |
| return gerrithome; | |
| } | |
| } | |
| /** | |
| * Check whether the process is running in Eclipse. | |
| * | |
| * <p>Unlike {@link #getDeveloperEclipseOut()}, this method checks the actual runtime stack, not | |
| * the classpath. | |
| * | |
| * @return true if any thread has a stack frame in {@code org.eclipse.jdt}. | |
| */ | |
| public static boolean isRunningInEclipse() { | |
| return Thread.getAllStackTraces().values().stream() | |
| .flatMap(Arrays::stream) | |
| .anyMatch(e -> e.getClassName().startsWith("org.eclipse.jdt.")); | |
| } | |
| /** | |
| * Locate the path of the {@code eclipse-out} directory in a source tree. | |
| * | |
| * <p>Unlike {@link #isRunningInEclipse()}, this method only inspects files relative to the | |
| * classpath, not the runtime stack. | |
| * | |
| * @return local path of the {@code eclipse-out} directory in a source tree. | |
| * @throws FileNotFoundException if the directory cannot be found. | |
| */ | |
| public static Path getDeveloperEclipseOut() throws FileNotFoundException { | |
| return resolveInSourceRoot("eclipse-out"); | |
| } | |
| public static boolean isJdk9OrLater() { | |
| return Double.parseDouble(System.getProperty("java.class.version")) >= 53.0; | |
| } | |
| public static String getJdkVersionPostJdk8() { | |
| // 9.0.4 => 9 | |
| return System.getProperty("java.version").substring(0, 1); | |
| } | |
| public static Properties loadBuildProperties(Path propPath) throws IOException { | |
| Properties properties = new Properties(); | |
| try (InputStream in = Files.newInputStream(propPath)) { | |
| properties.load(in); | |
| } catch (NoSuchFileException e) { | |
| // Ignore; will be run from PATH, with a descriptive error if it fails. | |
| } | |
| return properties; | |
| } | |
| static final String SOURCE_ROOT_RESOURCE = "/com/google/gerrit/launcher/workspace-root.txt"; | |
| /** | |
| * Locate a path in the source tree. | |
| * | |
| * @return local path of the {@code name} directory in a source tree. | |
| * @throws FileNotFoundException if the directory cannot be found. | |
| */ | |
| public static Path resolveInSourceRoot(String name) throws FileNotFoundException { | |
| // Find ourselves in the classpath, as a loose class file or jar. | |
| Class<GerritLauncher> self = GerritLauncher.class; | |
| Path dir; | |
| String sourceRoot = System.getProperty("sourceRoot"); | |
| if (sourceRoot != null) { | |
| dir = Paths.get(sourceRoot); | |
| if (!Files.exists(dir)) { | |
| throw new FileNotFoundException("source root not found: " + dir); | |
| } | |
| } else { | |
| URL u = self.getResource(self.getSimpleName() + ".class"); | |
| if (u == null) { | |
| throw new FileNotFoundException("Cannot find class " + self.getName()); | |
| } else if ("jar".equals(u.getProtocol())) { | |
| String p = u.getPath(); | |
| try { | |
| u = new URL(p.substring(0, p.indexOf('!'))); | |
| } catch (MalformedURLException e) { | |
| FileNotFoundException fnfe = new FileNotFoundException("Not a valid jar file: " + u); | |
| fnfe.initCause(e); | |
| throw fnfe; | |
| } | |
| } | |
| if (!"file".equals(u.getProtocol())) { | |
| throw new FileNotFoundException("Cannot extract path from " + u); | |
| } | |
| // Pop up to the top-level source folder by looking for WORKSPACE. | |
| dir = Paths.get(u.getPath()); | |
| while (!Files.isRegularFile(dir.resolve("WORKSPACE"))) { | |
| Path parent = dir.getParent(); | |
| if (parent == null) { | |
| throw new FileNotFoundException("Cannot find source root from " + u); | |
| } | |
| dir = parent; | |
| } | |
| } | |
| Path ret = dir.resolve(name); | |
| if (!Files.exists(ret)) { | |
| throw new FileNotFoundException(name + " not found in source root " + dir); | |
| } | |
| return ret; | |
| } | |
| private static ClassLoader useDevClasspath() throws IOException { | |
| Path out = getDeveloperEclipseOut(); | |
| List<URL> dirs = new ArrayList<>(); | |
| dirs.add(out.resolve("classes").toUri().toURL()); | |
| ClassLoader cl = GerritLauncher.class.getClassLoader(); | |
| if (isJdk9OrLater()) { | |
| Path rootPath = resolveInSourceRoot(".").normalize(); | |
| Properties properties = loadBuildProperties(rootPath.resolve(".bazel_path")); | |
| Path outputBase = Paths.get(properties.getProperty("output_base")); | |
| Path runtimeClasspath = | |
| rootPath.resolve("bazel-bin/tools/eclipse/main_classpath_collect.runtime_classpath"); | |
| for (String f : Files.readAllLines(runtimeClasspath, UTF_8)) { | |
| URL url; | |
| if (f.startsWith("external")) { | |
| url = outputBase.resolve(f).toUri().toURL(); | |
| } else { | |
| url = rootPath.resolve(f).toUri().toURL(); | |
| } | |
| if (includeJar(url)) { | |
| dirs.add(url); | |
| } | |
| } | |
| } else { | |
| for (URL u : ((URLClassLoader) cl).getURLs()) { | |
| if (includeJar(u)) { | |
| dirs.add(u); | |
| } | |
| } | |
| } | |
| return URLClassLoader.newInstance( | |
| dirs.toArray(new URL[dirs.size()]), ClassLoader.getSystemClassLoader().getParent()); | |
| } | |
| private static boolean includeJar(URL u) { | |
| String path = u.getPath(); | |
| return path.endsWith(".jar") | |
| && !path.endsWith("-src.jar") | |
| && !path.contains("/com/google/gerrit"); | |
| } | |
| private GerritLauncher() {} | |
| } |