Introduce Gerrit Inspector: interactive Jython shell

Adds "-s" option to com.google.gerrit.pgm.Daemon to start an interactive
Jython shell for inspection and troubleshooting of live data of the
Gerrit instance.

Gerrit Inspector is an interactive scriptable environment to inspect and
modify internal state of the system. This environment is available on
the system console after the system starts. Leaving the Inspector will
shutdown the Gerrit instance.

The environment allows interactive work as well as running of Python
scripts for troubleshooting. Accessing Java Virtual Machine objects
and calling Java methods is possible.

Gerrit Inspect can be started by adding -s option to the command used to
launch the daemon:

  java -jar buck-out/gen/gerrit.war daemon -d ../test_site -s

For more information on this facility please see:

  Documentation/dev-inspector.txt

Implementation remark:

Jython needs to examine available .jar files on startup. It does so
currently by reading contents of the WAR archive unpacked by GerritLauncher
in the temporary directory. Jython is able to store this information
persistently, but it does not currently work because we unpack WAR
file every time on startup.

Currently no attempt is made to introspect Guice bindings.

Change-Id: I47ffc8383fd50bc6ec12ba31edb6d7d614e97bd5
This commit is contained in:
Marcin Cieślak
2012-04-17 16:24:34 +00:00
committed by Shawn Pearce
parent aebbe03e4f
commit ed612fb44a
7 changed files with 672 additions and 31 deletions

View File

@@ -33,6 +33,7 @@ import com.google.gerrit.pgm.http.jetty.GetUserFilter;
import com.google.gerrit.pgm.http.jetty.JettyEnv;
import com.google.gerrit.pgm.http.jetty.JettyModule;
import com.google.gerrit.pgm.http.jetty.ProjectQoSFilter;
import com.google.gerrit.pgm.shell.JythonShell;
import com.google.gerrit.pgm.util.ErrorLogFile;
import com.google.gerrit.pgm.util.GarbageCollectionLogFile;
import com.google.gerrit.pgm.util.LogFileCompressor;
@@ -57,6 +58,7 @@ import com.google.gerrit.server.mail.SmtpEmailSender;
import com.google.gerrit.server.patch.IntraLineWorkerPool;
import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
import com.google.gerrit.server.plugins.PluginRestApiModule;
import com.google.gerrit.server.schema.DataSourceProvider;
import com.google.gerrit.server.schema.SchemaVersionCheck;
import com.google.gerrit.server.ssh.NoSshKeyCache;
import com.google.gerrit.server.ssh.NoSshModule;
@@ -109,6 +111,9 @@ public class Daemon extends SiteProgram {
@Option(name = "--console-log", usage = "Log to console (not $site_path/logs)")
private boolean consoleLog;
@Option(name = "-s", usage = "Start interactive shell")
private boolean inspector;
@Option(name = "--run-id", usage = "Cookie to store in $site_path/logs/gerrit.run")
private String runId;
@@ -224,7 +229,15 @@ public class Daemon extends SiteProgram {
serverStarted.run();
}
RuntimeShutdown.waitFor();
if (inspector) {
JythonShell shell = new JythonShell();
shell.set("m", manager);
shell.set("ds", dbInjector.getInstance(DataSourceProvider.class));
shell.set("schk", dbInjector.getInstance(SchemaVersionCheck.class));
shell.run();
} else {
RuntimeShutdown.waitFor();
}
return 0;
} catch (Throwable err) {
log.error("Unable to start daemon", err);

View File

@@ -0,0 +1,221 @@
// Copyright (C) 2013 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.pgm.shell;
import com.google.gerrit.launcher.GerritLauncher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Properties;
public class JythonShell {
private static final Logger log = LoggerFactory.getLogger(JythonShell.class);
private static final String STARTUP_RESOURCE = "com/google/gerrit/pgm/Startup.py";
private static final String STARTUP_FILE = "Startup.py";
private Class<?> console;
private Class<?> pyObject;
private Class<?> pySystemState;
private Object shell;
private ArrayList <String> injectedVariables;
public JythonShell() {
Properties env = new Properties();
// Let us inspect private class members
env.setProperty("python.security.respectJavaAccessibility", "false");
File home = GerritLauncher.getHomeDirectory();
if (home != null) {
env.setProperty("python.cachedir", new File(home, "jythoncache").getPath());
}
// For package introspection and "import com.google" to work,
// Jython needs to inspect actual .jar files (not just classloader)
StringBuilder classPath = new StringBuilder();
final ClassLoader cl = getClass().getClassLoader();
if (cl instanceof java.net.URLClassLoader) {
URLClassLoader ucl = (URLClassLoader) cl;
for (URL u : ucl.getURLs()) {
if ("file".equals(u.getProtocol())) {
if (classPath.length() > 0) {
classPath.append(java.io.File.pathSeparatorChar);
}
classPath.append(u.getFile());
}
}
}
env.setProperty("java.class.path", classPath.toString());
console = findClass("org.python.util.InteractiveConsole");
pyObject = findClass("org.python.core.PyObject");
pySystemState = findClass("org.python.core.PySystemState");
runMethod(pySystemState, pySystemState, "initialize",
new Class[] { Properties.class, Properties.class },
new Object[] { null, env }
);
try {
shell = console.newInstance();
log.info("Jython shell instance created.");
} catch (InstantiationException e) {
throw noInterpreter(e);
} catch (IllegalAccessException e) {
throw noInterpreter(e);
}
injectedVariables = new ArrayList<String>();
set("Shell", this);
}
protected Object runMethod0(Class<?> klazz, Object instance,
String name, Class<?>[] sig, Object[] args)
throws InvocationTargetException {
try {
Method m;
m = klazz.getMethod(name, sig);
return m.invoke(instance, args);
} catch (NoSuchMethodException e) {
throw cannotStart(e);
} catch (SecurityException e) {
throw cannotStart(e);
} catch (IllegalArgumentException e) {
throw cannotStart(e);
} catch (IllegalAccessException e) {
throw cannotStart(e);
}
}
protected Object runMethod(Class<?> klazz, Object instance,
String name, Class<?>[] sig, Object[] args) {
try {
return runMethod0(klazz, instance, name, sig, args);
} catch (InvocationTargetException e) {
throw cannotStart(e);
}
}
protected Object runInterpreter(String name, Class<?>[] sig, Object[] args) {
return runMethod(console, shell, name, sig, args);
}
protected String getDefaultBanner() {
return (String)runInterpreter("getDefaultBanner",
new Class[] { }, new Object[] { });
}
protected void printInjectedVariable(String id) {
runInterpreter("exec",
new Class[] { String.class },
new Object[] { "print '\"%s\" is \"%s\"' % (\"" + id + "\", " + id + ")" }
);
}
public void run() {
for (String key : injectedVariables) {
printInjectedVariable(key);
}
reload();
runInterpreter("interact",
new Class[] { String.class, pyObject },
new Object[] { getDefaultBanner() +
" running for Gerrit " + com.google.gerrit.common.Version.getVersion(),
null });
}
public void set(String key, Object content) {
runInterpreter("set",
new Class[] { String.class, Object.class },
new Object[] { key, content }
);
injectedVariables.add(key);
}
private static Class<?> findClass(String klazzname) {
try {
return Class.forName(klazzname);
} catch (ClassNotFoundException e) {
throw noShell("Class " + klazzname + " not found", e);
}
}
public void reload() {
execResource(STARTUP_RESOURCE);
execFile(GerritLauncher.getHomeDirectory(), STARTUP_FILE);
}
protected void execResource(final String p) {
InputStream in = JythonShell.class.getClassLoader().getResourceAsStream(p);
if (in != null) {
execStream(in, "resource " + p);
} else {
log.error("Cannot load resource " + p);
}
}
protected void execFile(final File parent, final String p) {
try {
File script = new File(parent, p);
if (script.canExecute()) {
runMethod0(console, shell, "execfile",
new Class[] { String.class },
new Object[] { script.getAbsolutePath() }
);
} else {
log.info("User initialization file "
+ script.getAbsolutePath()
+ " is not found or not executable");
}
} catch (InvocationTargetException e) {
log.error("Exception occured while loading file " + p + " : ", e);
} catch (SecurityException e) {
log.error("SecurityException occured while loading file " + p + " : ", e);
}
}
protected void execStream(final InputStream in, final String p) {
try {
runMethod0(console, shell, "execfile",
new Class[] { InputStream.class, String.class },
new Object[] { in, p }
);
} catch (InvocationTargetException e) {
log.error("Exception occured while loading " + p + " : ", e);
}
}
private static UnsupportedOperationException noShell(final String m, Throwable why) {
final String prefix = "Cannot create Jython shell: ";
final String postfix = "\n (You might need to install jython.jar in the lib directory)";
return new UnsupportedOperationException(prefix + m + postfix, why);
}
private static UnsupportedOperationException noInterpreter(Throwable why) {
final String msg = "Cannot create Python interpreter";
return noShell(msg, why);
}
private static UnsupportedOperationException cannotStart(Throwable why) {
final String msg = "Cannot start Jython shell";
return new UnsupportedOperationException(msg, why);
}
}