| // 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.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 = Path.of(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 = Path.of(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 = Path.of(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() {} |
| } |