blob: 4ee9676ac9984a3e5ef4b58abdf550dceb9cab3a [file] [log] [blame]
// 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.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
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.URL;
import java.net.URLClassLoader;
import java.security.CodeSource;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
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";
public static void main(final String argv[]) throws Exception {
System.exit(mainImpl(argv));
}
public static int mainImpl(final 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();
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();
Thread.currentThread().setContextClassLoader(cl);
return invokeProgram(cl, argv);
}
private static String getVersion(final File me) {
if (me == null) {
return "";
}
try {
final JarFile jar = new JarFile(me);
try {
Manifest mf = jar.getManifest();
Attributes att = mf.getMainAttributes();
String val = att.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
return val != null ? val : "";
} finally {
jar.close();
}
} catch (IOException e) {
return "";
}
}
private static int invokeProgram(final ClassLoader loader,
final 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 = name;
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));
}
}
cn = buf.toString();
}
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.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();
} else {
return 0;
}
}
private static ClassLoader libClassLoader() 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 {
final ZipFile zf = new ZipFile(path);
try {
final Enumeration<? extends ZipEntry> e = zf.entries();
while (e.hasMoreElements()) {
final ZipEntry ze = e.nextElement();
if (ze.isDirectory()) {
continue;
} else if (ze.getName().startsWith("WEB-INF/lib/")) {
extractJar(zf, ze, jars);
} else if (ze.getName().startsWith("WEB-INF/pgm-lib/")) {
extractJar(zf, ze, jars);
}
}
} finally {
zf.close();
}
} 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 = new URLClassLoader(
extapi.toArray(new URL[extapi.size()]),
parent);
}
return new URLClassLoader(
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");
FileOutputStream out = new FileOutputStream(tmp);
try {
InputStream in = zf.getInputStream(ze);
try {
byte[] buf = new byte[4096];
int n;
while ((n = in.read(buf, 0, buf.length)) > 0) {
out.write(buf, 0, n);
}
} finally {
in.close();
}
} finally {
out.close();
}
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(final 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;
/**
* 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 {
if (myArchive == null) {
myArchive = locateMyArchive();
}
return myArchive;
}
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 {
final InputStream in = src.getLocation().openStream();
try {
final File tmp = createTempFile("gerrit_", ".zip");
final FileOutputStream out = new FileOutputStream(tmp);
try {
final byte[] buf = new byte[4096];
int n;
while ((n = in.read(buf, 0, buf.length)) > 0) {
out.write(buf, 0, n);
}
} finally {
out.close();
}
return tmp;
} finally {
in.close();
}
} 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 (final 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;
}
}
/**
* Locate the path of the {@code buck-out} directory in a source tree.
*
* @throws FileNotFoundException if the directory cannot be found.
*/
public static File getDeveloperBuckOut() throws FileNotFoundException {
// Find ourselves in the CLASSPATH, we should be a loose class file.
Class<GerritLauncher> self = GerritLauncher.class;
URL u = self.getResource(self.getSimpleName() + ".class");
if (u == null) {
throw new FileNotFoundException("Cannot find class " + self.getName());
} else if (!"file".equals(u.getProtocol())) {
throw new FileNotFoundException("Cannot find extract path from " + u);
}
// Pop up to the top level classes folder that contains us.
File dir = new File(u.getPath());
String myName = self.getName();
for (;;) {
int dot = myName.lastIndexOf('.');
if (dot < 0) {
dir = dir.getParentFile();
break;
}
myName = myName.substring(0, dot);
dir = dir.getParentFile();
}
dir = popdir(u, dir, "classes");
dir = popdir(u, dir, "eclipse");
if ("buck-out".equals(dir.getName())) {
return dir;
}
throw new FileNotFoundException("Cannot find buck-out from " + u);
}
private static File popdir(URL u, File dir, String name)
throws FileNotFoundException {
if (dir.getName().equals(name)) {
return dir.getParentFile();
}
throw new FileNotFoundException("Cannot find buck-out from " + u);
}
private static ClassLoader useDevClasspath()
throws MalformedURLException, FileNotFoundException {
File out = getDeveloperBuckOut();
List<URL> dirs = new ArrayList<>();
dirs.add(new File(new File(out, "eclipse"), "classes").toURI().toURL());
ClassLoader cl = GerritLauncher.class.getClassLoader();
for (URL u : ((URLClassLoader) cl).getURLs()) {
if (includeJar(u)) {
dirs.add(u);
}
}
return new URLClassLoader(
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("/buck-out/gen/lib/gwt/");
}
private GerritLauncher() {
}
}