Add in "plugin" support to allow users to add their own plugin's via JAR files and the -plugin command line argument.
Also make sure that the arguments passed on the command line correctly get set in sys.argv.

Change-Id: I35014adc95ac9e5e5c777dc17749ee2b9b155c57
diff --git a/tools/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunner.java b/tools/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunner.java
index 85dbc7c..d144780 100644
--- a/tools/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunner.java
+++ b/tools/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunner.java
@@ -91,8 +91,11 @@
     @MonkeyRunnerExported(doc = "Simple help command to dump the MonkeyRunner supported " +
             "commands",
             returns = "The help text")
-    public static String help(PyObject[] args, String[] kws) {
-      return MonkeyRunnerHelp.helpString();
+            public static String help(PyObject[] args, String[] kws) {
+        ArgParser ap = JythonUtils.createArgParser(args, kws);
+        Preconditions.checkNotNull(ap);
+
+        return MonkeyRunnerHelp.helpString();
     }
 
     @MonkeyRunnerExported(doc = "Put up an alert dialog to inform the user of something that " +
diff --git a/tools/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunningOptions.java b/tools/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerOptions.java
similarity index 65%
rename from tools/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunningOptions.java
rename to tools/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerOptions.java
index 3215c31..68577a5 100644
--- a/tools/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunningOptions.java
+++ b/tools/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerOptions.java
@@ -15,12 +15,15 @@
  */
 package com.android.monkeyrunner;
 
+import com.google.common.collect.ImmutableList;
+
 import java.io.File;
+import java.util.Collection;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
-public class MonkeyRunningOptions {
-    private static final Logger LOG = Logger.getLogger(MonkeyRunningOptions.class.getName());
+public class MonkeyRunnerOptions {
+    private static final Logger LOG = Logger.getLogger(MonkeyRunnerOptions.class.getName());
     private static String DEFAULT_MONKEY_SERVER_ADDRESS = "127.0.0.1";
     private static int DEFAULT_MONKEY_PORT = 12345;
 
@@ -28,12 +31,17 @@
     private final String hostname;
     private final File scriptFile;
     private final String backend;
+    private final Collection<File> plugins;
+    private final Collection<String> arguments;
 
-    private MonkeyRunningOptions(String hostname, int port, File scriptFile, String backend) {
+    private MonkeyRunnerOptions(String hostname, int port, File scriptFile, String backend,
+            Collection<File> plugins, Collection<String> arguments) {
         this.hostname = hostname;
         this.port = port;
         this.scriptFile = scriptFile;
         this.backend = backend;
+        this.plugins = plugins;
+        this.arguments = arguments;
     }
 
     public int getPort() {
@@ -52,6 +60,14 @@
         return backend;
     }
 
+    public Collection<File> getPlugins() {
+        return plugins;
+    }
+
+    public Collection<String> getArguments() {
+        return arguments;
+    }
+
     private static void printUsage(String message) {
         System.out.println(message);
         System.out.println("Usage: monkeyrunner [options] SCRIPT_FILE");
@@ -68,7 +84,7 @@
      *
      * @return the parsed options, or null if there was an error.
      */
-    public static MonkeyRunningOptions processOptions(String[] args) {
+    public static MonkeyRunnerOptions processOptions(String[] args) {
         // parse command line parameters.
         int index = 0;
 
@@ -77,6 +93,8 @@
         int port = DEFAULT_MONKEY_PORT;
         String backend = "adb";
 
+        ImmutableList.Builder<File> pluginListBuilder = ImmutableList.builder();
+        ImmutableList.Builder<String> argumentBuilder = ImmutableList.builder();
         do {
             String argument = args[index++];
 
@@ -114,16 +132,42 @@
                     return null;
                 }
                 backend = args[index++];
+            } else if ("-plugin".equals(argument)) {
+                // quick check on the next argument.
+                if (index == args.length) {
+                    printUsage("Missing plugin path after -plugin");
+                    return null;
+                }
+                File plugin = new File(args[index++]);
+                if (!plugin.exists()) {
+                    printUsage("Plugin file doesn't exist");
+                    return null;
+                }
+
+                if (!plugin.canRead()) {
+                    printUsage("Can't read plugin file");
+                    return null;
+                }
+
+                pluginListBuilder.add(plugin);
             } else if (argument.startsWith("-")) {
                 // we have an unrecognized argument.
                 printUsage("Unrecognized argument: " + argument + ".");
                 return null;
             } else {
-                // get the filepath of the script to run.  This will be the last undashed argument.
-                scriptFile = new File(argument);
-                if (!scriptFile.exists()) {
-                    printUsage("Can't open specified script file");
-                    return null;
+                if (scriptFile == null) {
+                    // get the filepath of the script to run.  This will be the last undashed argument.
+                    scriptFile = new File(argument);
+                    if (!scriptFile.exists()) {
+                        printUsage("Can't open specified script file");
+                        return null;
+                    }
+                    if (!scriptFile.canRead()) {
+                        printUsage("Can't open specified script file");
+                        return null;
+                    }
+                } else {
+                    argumentBuilder.add(argument);
                 }
             }
         } while (index < args.length);
@@ -133,6 +177,7 @@
             return null;
         }
 
-        return new MonkeyRunningOptions(hostname, port, scriptFile, backend);
+        return new MonkeyRunnerOptions(hostname, port, scriptFile, backend,
+                pluginListBuilder.build(), argumentBuilder.build());
     }
 }
diff --git a/tools/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerStarter.java b/tools/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerStarter.java
index e2fdee9..54bb7da 100644
--- a/tools/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerStarter.java
+++ b/tools/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerStarter.java
@@ -15,10 +15,25 @@
  */
 package com.android.monkeyrunner;
 
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableMap;
+
 import com.android.monkeyrunner.adb.AdbBackend;
+import com.android.monkeyrunner.stub.StubBackend;
+
+import org.python.util.PythonInterpreter;
 
 import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
 import java.util.Enumeration;
+import java.util.Map;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
 import java.util.logging.Formatter;
 import java.util.logging.Handler;
 import java.util.logging.Level;
@@ -33,9 +48,19 @@
  */
 public class MonkeyRunnerStarter {
     private static final Logger LOG = Logger.getLogger(MonkeyRunnerStarter.class.getName());
+    private static final String MONKEY_RUNNER_MAIN_MANIFEST_NAME = "MonkeyRunnerStartupRunner";
 
     private final MonkeyRunnerBackend backend;
-    private final File scriptFile;
+    private final MonkeyRunnerOptions options;
+
+    public MonkeyRunnerStarter(MonkeyRunnerOptions options) {
+        this.options = options;
+        this.backend = MonkeyRunnerStarter.createBackendByName(options.getBackendName());
+        if (this.backend == null) {
+           throw new RuntimeException("Unknown backend");
+        }
+    }
+
 
     /**
      * Creates a specific backend by name.
@@ -43,30 +68,103 @@
      * @param backendName the name of the backend to create
      * @return the new backend, or null if none were found.
      */
-    public MonkeyRunnerBackend createBackendByName(String backendName) {
+    public static MonkeyRunnerBackend createBackendByName(String backendName) {
         if ("adb".equals(backendName)) {
             return new AdbBackend();
+        } else if ("stub".equals(backendName)) {
+            return new StubBackend();
         } else {
             return null;
         }
     }
 
-    public MonkeyRunnerStarter(String backendName,
-            File scriptFile) {
-        this.backend = createBackendByName(backendName);
-        if (this.backend == null) {
-            throw new RuntimeException("Unknown backend");
-        }
-        this.scriptFile = scriptFile;
-    }
-
     private void run() {
         MonkeyRunner.setBackend(backend);
-        ScriptRunner.run(scriptFile.getAbsolutePath());
+        Map<String, Predicate<PythonInterpreter>> plugins = handlePlugins();
+        ScriptRunner.run(options.getScriptFile().getAbsolutePath(),
+                         options.getArguments(), plugins);
         backend.shutdown();
         MonkeyRunner.setBackend(null);
     }
 
+    private Predicate<PythonInterpreter> handlePlugin(File f) {
+        JarFile jarFile;
+        try {
+            jarFile = new JarFile(f);
+        } catch (IOException e) {
+            LOG.log(Level.SEVERE, "Unable to open plugin file.  Is it a jar file? " +
+                    f.getAbsolutePath(), e);
+            return Predicates.alwaysFalse();
+        }
+        Manifest manifest;
+        try {
+            manifest = jarFile.getManifest();
+        } catch (IOException e) {
+            LOG.log(Level.SEVERE, "Unable to get manifest file from jar: " +
+                    f.getAbsolutePath(), e);
+            return Predicates.alwaysFalse();
+        }
+        Attributes mainAttributes = manifest.getMainAttributes();
+        String pluginClass = mainAttributes.getValue(MONKEY_RUNNER_MAIN_MANIFEST_NAME);
+        if (pluginClass == null) {
+            // No main in this plugin, so it always succeeds.
+            return Predicates.alwaysTrue();
+        }
+        URL url;
+        try {
+            url =  f.toURI().toURL();
+        } catch (MalformedURLException e) {
+            LOG.log(Level.SEVERE, "Unable to convert file to url " + f.getAbsolutePath(),
+                    e);
+            return Predicates.alwaysFalse();
+        }
+        URLClassLoader classLoader = new URLClassLoader(new URL[] { url },
+                ClassLoader.getSystemClassLoader());
+        Class<?> clz;
+        try {
+            clz = Class.forName(pluginClass, true, classLoader);
+        } catch (ClassNotFoundException e) {
+            LOG.log(Level.SEVERE, "Unable to load the specified plugin: " + pluginClass, e);
+            return Predicates.alwaysFalse();
+        }
+        Object loadedObject;
+        try {
+            loadedObject = clz.newInstance();
+        } catch (InstantiationException e) {
+            LOG.log(Level.SEVERE, "Unable to load the specified plugin: " + pluginClass, e);
+            return Predicates.alwaysFalse();
+        } catch (IllegalAccessException e) {
+            LOG.log(Level.SEVERE, "Unable to load the specified plugin " +
+                    "(did you make it public?): " + pluginClass, e);
+            return Predicates.alwaysFalse();
+        }
+        // Cast it to the right type
+        if (loadedObject instanceof Runnable) {
+            final Runnable run = (Runnable) loadedObject;
+            return new Predicate<PythonInterpreter>() {
+                public boolean apply(PythonInterpreter i) {
+                    run.run();
+                    return true;
+                }
+            };
+        } else if (loadedObject instanceof Predicate<?>) {
+            return (Predicate<PythonInterpreter>) loadedObject;
+        } else {
+            LOG.severe("Unable to coerce object into correct type: " + pluginClass);
+            return Predicates.alwaysFalse();
+        }
+    }
+
+    private Map<String, Predicate<PythonInterpreter>> handlePlugins() {
+        ImmutableMap.Builder<String, Predicate<PythonInterpreter>> builder = ImmutableMap.builder();
+        for (File f : options.getPlugins()) {
+            builder.put(f.getAbsolutePath(), handlePlugin(f));
+        }
+        return builder.build();
+    }
+
+
+
     private static final void replaceAllLogFormatters(Formatter form) {
         LogManager mgr = LogManager.getLogManager();
         Enumeration<String> loggerNames = mgr.getLoggerNames();
@@ -81,7 +179,7 @@
     }
 
     public static void main(String[] args) {
-        MonkeyRunningOptions options = MonkeyRunningOptions.processOptions(args);
+        MonkeyRunnerOptions options = MonkeyRunnerOptions.processOptions(args);
 
         // logging property files are difficult
         replaceAllLogFormatters(MonkeyFormatter.DEFAULT_INSTANCE);
@@ -90,8 +188,7 @@
             return;
         }
 
-        MonkeyRunnerStarter runner = new MonkeyRunnerStarter(options.getBackendName(),
-                options.getScriptFile());
+        MonkeyRunnerStarter runner = new MonkeyRunnerStarter(options);
         runner.run();
 
         // This will kill any background threads as well.
diff --git a/tools/monkeyrunner/src/com/android/monkeyrunner/ScriptRunner.java b/tools/monkeyrunner/src/com/android/monkeyrunner/ScriptRunner.java
index c027be8..7920e50 100644
--- a/tools/monkeyrunner/src/com/android/monkeyrunner/ScriptRunner.java
+++ b/tools/monkeyrunner/src/com/android/monkeyrunner/ScriptRunner.java
@@ -15,7 +15,10 @@
  */
 package com.android.monkeyrunner;
 
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
+import com.google.common.collect.ImmutableMap.Builder;
 
 import org.python.core.PyObject;
 import org.python.util.InteractiveConsole;
@@ -23,15 +26,21 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Properties;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 
 /**
  * Runs Jython based scripts.
  */
 public class ScriptRunner {
+    private static final Logger LOG = Logger.getLogger(MonkeyRunnerOptions.class.getName());
 
     /** The "this" scope object for scripts. */
     private final Object scope;
@@ -51,20 +60,46 @@
     /**
      * Runs the specified Jython script. First runs the initialization script to
      * preload the appropriate client library version.
+     *
+     * @param scriptfilename the name of the file to run.
+     * @param args the arguments passed in (excluding the filename).
+     * @param plugins a list of plugins to load.
      */
-    public static void run(String scriptfilename) {
-        try {
-            // Add the current directory of the script to the python.path search path.
-            File f = new File(scriptfilename);
-            initPython(Lists.newArrayList(f.getParent()),
-                    new String[] { f.getCanonicalPath() });
+    public static void run(String scriptfilename, Collection<String> args,
+            Map<String, Predicate<PythonInterpreter>> plugins) {
+        // Add the current directory of the script to the python.path search path.
+        File f = new File(scriptfilename);
 
-            PythonInterpreter python = new PythonInterpreter();
+        // Adjust the classpath so jython can access the classes in the specified classpath.
+        Collection<String> classpath = Lists.newArrayList(f.getParent());
+        classpath.addAll(plugins.keySet());
 
-            python.execfile(scriptfilename);
-        } catch(Exception e) {
-            e.printStackTrace();
+        String[] argv = new String[args.size() + 1];
+        argv[0] = f.getAbsolutePath();
+        int x = 1;
+        for (String arg : args) {
+            argv[x++] = arg;
         }
+
+        initPython(classpath, argv);
+
+        PythonInterpreter python = new PythonInterpreter();
+
+        // Now let the mains run.
+        for (Map.Entry<String, Predicate<PythonInterpreter>> entry : plugins.entrySet()) {
+            boolean success;
+            try {
+                success = entry.getValue().apply(python);
+            } catch (Exception e) {
+                LOG.log(Level.SEVERE, "Plugin Main through an exception.", e);
+                continue;
+            }
+            if (!success) {
+                LOG.severe("Plugin Main returned error for: " + entry.getKey());
+            }
+        }
+
+        python.execfile(scriptfilename);
     }
 
     public static void runString(String script) {
@@ -73,11 +108,20 @@
         python.exec(script);
     }
 
-    public static PyObject runStringAndGet(String script, String name) {
+    public static Map<String, PyObject> runStringAndGet(String script, String... names) {
+        return runStringAndGet(script, Arrays.asList(names));
+    }
+
+    public static Map<String, PyObject> runStringAndGet(String script, Collection<String> names) {
         initPython();
-        PythonInterpreter python = new PythonInterpreter();
+        final PythonInterpreter python = new PythonInterpreter();
         python.exec(script);
-        return python.get(name);
+
+        Builder<String, PyObject> builder = ImmutableMap.builder();
+        for (String name : names) {
+            builder.put(name, python.get(name));
+        }
+        return builder.build();
     }
 
     private static void initPython() {
@@ -85,7 +129,7 @@
         initPython(arg, new String[] {""});
     }
 
-    private static void initPython(List<String> pythonPath,
+    private static void initPython(Collection<String> pythonPath,
             String[] argv) {
         Properties props = new Properties();
 
diff --git a/tools/monkeyrunner/src/com/android/monkeyrunner/exceptions/MonkeyRunnerException.java b/tools/monkeyrunner/src/com/android/monkeyrunner/exceptions/MonkeyRunnerException.java
new file mode 100644
index 0000000..1d5f19b
--- /dev/null
+++ b/tools/monkeyrunner/src/com/android/monkeyrunner/exceptions/MonkeyRunnerException.java
@@ -0,0 +1,18 @@
+package com.android.monkeyrunner.exceptions;
+
+/**
+ * Base exception class for all MonkeyRunner Exceptions.
+ */
+public class MonkeyRunnerException extends Exception {
+    public MonkeyRunnerException(String message) {
+        super(message);
+    }
+
+    public MonkeyRunnerException(Throwable e) {
+        super(e);
+    }
+
+    public MonkeyRunnerException(String message, Throwable e) {
+        super(message, e);
+    }
+}
diff --git a/tools/monkeyrunner/src/com/android/monkeyrunner/stub/StubBackend.java b/tools/monkeyrunner/src/com/android/monkeyrunner/stub/StubBackend.java
index 7edff69..9b23bab 100644
--- a/tools/monkeyrunner/src/com/android/monkeyrunner/stub/StubBackend.java
+++ b/tools/monkeyrunner/src/com/android/monkeyrunner/stub/StubBackend.java
@@ -4,19 +4,15 @@
 import com.android.monkeyrunner.MonkeyManager;
 import com.android.monkeyrunner.MonkeyRunnerBackend;
 
-/**
- * This is a stub backend that doesn't do anything at all.  Useful for
- * running unit tests.
- */
 public class StubBackend implements MonkeyRunnerBackend {
 
     public MonkeyManager createManager(String address, int port) {
-        // We're stub - we've got nothing to do.
+        // TODO Auto-generated method stub
         return null;
     }
 
     public MonkeyDevice waitForConnection(long timeout, String deviceId) {
-        // We're stub - we've got nothing to do.
+        // TODO Auto-generated method stub
         return null;
     }
 
diff --git a/tools/monkeyrunner/test/com/android/monkeyrunner/JythonUtilsTest.java b/tools/monkeyrunner/test/com/android/monkeyrunner/JythonUtilsTest.java
index 6e3260d..5b8c8f9 100644
--- a/tools/monkeyrunner/test/com/android/monkeyrunner/JythonUtilsTest.java
+++ b/tools/monkeyrunner/test/com/android/monkeyrunner/JythonUtilsTest.java
@@ -102,7 +102,7 @@
         }
         sb.append(")");
 
-        return ScriptRunner.runStringAndGet(sb.toString(), "result");
+        return ScriptRunner.runStringAndGet(sb.toString(), "result").get("result");
     }
 
     public void testSimpleCall() {