summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/launcher/GerritLauncher.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/launcher/GerritLauncher.java')
-rw-r--r--java/com/google/gerrit/launcher/GerritLauncher.java774
1 files changed, 774 insertions, 0 deletions
diff --git a/java/com/google/gerrit/launcher/GerritLauncher.java b/java/com/google/gerrit/launcher/GerritLauncher.java
new file mode 100644
index 0000000000..e407d08870
--- /dev/null
+++ b/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -0,0 +1,774 @@
+// 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.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Scanner;
+import java.util.SortedMap;
+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";
+
+ private static ClassLoader daemonClassLoader;
+
+ public static void main(String[] argv) throws Exception {
+ System.exit(mainImpl(argv));
+ }
+
+ /**
+ * Invokes a proram.
+ *
+ * <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(" gsql Run the interactive query console");
+ 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 proram 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())) {
+ 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())) {
+ 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 SortedMap<String, URL> jars = new TreeMap<>();
+ try (ZipFile zf = new ZipFile(path)) {
+ final Enumeration<? extends ZipEntry> e = zf.entries();
+ while (e.hasMoreElements()) {
+ final ZipEntry ze = e.nextElement();
+ 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, "tomcat-servlet-api-", extapi);
+
+ ClassLoader parent = ClassLoader.getSystemClassLoader();
+ if (!extapi.isEmpty()) {
+ parent = URLClassLoader.newInstance(extapi.toArray(new URL[extapi.size()]), parent);
+ }
+ return URLClassLoader.newInstance(jars.values().toArray(new URL[jars.size()]), parent);
+ }
+
+ private static void extractJar(ZipFile zf, ZipEntry ze, SortedMap<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('/'), name.length()), tmp.toURI().toURL());
+ }
+
+ private static void move(SortedMap<String, URL> jars, String prefix, List<URL> extapi) {
+ SortedMap<String, URL> matches = jars.tailMap(prefix);
+ 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;
+ }
+
+ 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;
+ }
+ }
+
+ 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;
+
+ // If the build system provides us with a source root, use that.
+ try (InputStream stream = self.getResourceAsStream(SOURCE_ROOT_RESOURCE)) {
+ if (stream != null) {
+ try (Scanner scan = new Scanner(stream, UTF_8.name()).useDelimiter("\n")) {
+ if (scan.hasNext()) {
+ Path p = Paths.get(scan.next());
+ if (!Files.exists(p)) {
+ throw new FileNotFoundException("source root not found: " + p);
+ }
+ return p;
+ }
+ }
+ }
+ } catch (IOException e) {
+ // not Bazel, then.
+ }
+
+ 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 .buckconfig.
+ Path 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() {}
+}