
To create .classpath with java 9, run: $ tools/eclipse/project.py --java 9 Start Eclipse, and follow the instructions provided in dev-eclipse.txt to switch to Java 9 or later. To run gerrit from Eclipse, the system classpath is cast to URLClassLoader to retrieve the needed JARs. In Java 9 system classpath cannot be cast to URLClassLoader. To rectify, extract the classpath from Bazel. Change-Id: Ie341cd4dfee5520f689fc50f34aa5395b74b5e7d
777 lines
26 KiB
Java
777 lines
26 KiB
Java
// 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) {
|
|
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() {}
|
|
}
|