// 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. * *
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 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.
*
* 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.
*
* 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.
*
* 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.
*
* 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.
*
* 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