diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt
index 1cab648cfacd042afc378507b3305f040e74aa46..47a8aa96aae3eb1c141ca35410c2f87341dfe7e9 100644
--- a/indra/llcommon/CMakeLists.txt
+++ b/indra/llcommon/CMakeLists.txt
@@ -63,6 +63,7 @@ set(llcommon_SOURCE_FILES
     llheartbeat.cpp
     llinitparam.cpp
     llinstancetracker.cpp
+    llleap.cpp
     llliveappconfig.cpp
     lllivefile.cpp
     lllog.cpp
@@ -180,6 +181,7 @@ set(llcommon_HEADER_FILES
     llinstancetracker.h
     llkeythrottle.h
     lllazy.h
+    llleap.h
     lllistenerwrapper.h
     lllinkedqueue.h
     llliveappconfig.h
@@ -333,6 +335,7 @@ if (LL_TESTS)
   LL_ADD_INTEGRATION_TEST(stringize "" "${test_libs}")
   LL_ADD_INTEGRATION_TEST(lleventdispatcher "" "${test_libs}")
   LL_ADD_INTEGRATION_TEST(llprocess "" "${test_libs}")
+  LL_ADD_INTEGRATION_TEST(llleap "" "${test_libs}")
   LL_ADD_INTEGRATION_TEST(llstreamqueue "" "${test_libs}")
 
   # *TODO - reenable these once tcmalloc libs no longer break the build.
diff --git a/indra/llcommon/llleap.cpp b/indra/llcommon/llleap.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..dddf1286ac884eabc1d35114c157c48a586d54d9
--- /dev/null
+++ b/indra/llcommon/llleap.cpp
@@ -0,0 +1,379 @@
+/**
+ * @file   llleap.cpp
+ * @author Nat Goodspeed
+ * @date   2012-02-20
+ * @brief  Implementation for llleap.
+ * 
+ * $LicenseInfo:firstyear=2012&license=viewerlgpl$
+ * Copyright (c) 2012, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "llleap.h"
+// STL headers
+#include <sstream>
+#include <algorithm>
+// std headers
+// external library headers
+#include <boost/bind.hpp>
+#include <boost/scoped_ptr.hpp>
+#include <boost/tokenizer.hpp>
+// other Linden headers
+#include "llerror.h"
+#include "llstring.h"
+#include "llprocess.h"
+#include "llevents.h"
+#include "stringize.h"
+#include "llsdutil.h"
+#include "llsdserialize.h"
+
+LLLeap::LLLeap() {}
+LLLeap::~LLLeap() {}
+
+class LLLeapImpl: public LLLeap
+{
+public:
+    // Called only by LLLeap::create()
+    LLLeapImpl(const std::string& desc, const std::vector<std::string>& plugin):
+        // We might reassign mDesc in the constructor body if it's empty here.
+        mDesc(desc),
+        // We expect multiple LLLeapImpl instances. Definitely tweak
+        // mDonePump's name for uniqueness.
+        mDonePump("LLLeap", true),
+        // Troubling thought: what if one plugin intentionally messes with
+        // another plugin? LLEventPump names are in a single global namespace.
+        // Try to make that more difficult by generating a UUID for the reply-
+        // pump name -- so it should NOT need tweaking for uniqueness.
+        mReplyPump(LLUUID::generateNewID().asString()),
+        mExpect(0)
+    {
+        // Rule out empty vector
+        if (plugin.empty())
+        {
+            throw Error("no plugin command");
+        }
+
+        // Don't leave desc empty either, but in this case, if we weren't
+        // given one, we'll fake one.
+        if (desc.empty())
+        {
+            mDesc = LLProcess::basename(plugin[0]);
+            // how about a toLower() variant that returns the transformed string?!
+            std::string desclower(mDesc);
+            LLStringUtil::toLower(desclower);
+            // If we're running a Python script, use the script name for the
+            // desc instead of just 'python'. Arguably we should check for
+            // more different interpreters as well, but there's a reason to
+            // notice Python specially: we provide Python LLSD serialization
+            // support, so there's a pretty good reason to implement plugins
+            // in that language.
+            if (plugin.size() >= 2 && (desclower == "python" || desclower == "python.exe"))
+            {
+                mDesc = LLProcess::basename(plugin[1]);
+            }
+        }
+
+        // Listen for child "termination" right away to catch launch errors.
+        mDonePump.listen("LLLeap", boost::bind(&LLLeapImpl::bad_launch, this, _1));
+
+        // Okay, launch child.
+        LLProcess::Params params;
+        params.desc = mDesc;
+        std::vector<std::string>::const_iterator pi(plugin.begin()), pend(plugin.end());
+        params.executable = *pi++;
+        for ( ; pi != pend; ++pi)
+        {
+            params.args.add(*pi);
+        }
+        params.files.add(LLProcess::FileParam("pipe")); // stdin
+        params.files.add(LLProcess::FileParam("pipe")); // stdout
+        params.files.add(LLProcess::FileParam("pipe")); // stderr
+        params.postend = mDonePump.getName();
+        mChild = LLProcess::create(params);
+        // If that didn't work, no point in keeping this LLLeap object.
+        if (! mChild)
+        {
+            throw Error(STRINGIZE("failed to run " << mDesc));
+        }
+
+        // Okay, launch apparently worked. Change our mDonePump listener.
+        mDonePump.stopListening("LLLeap");
+        mDonePump.listen("LLLeap", boost::bind(&LLLeapImpl::done, this, _1));
+
+        // Child might pump large volumes of data through either stdout or
+        // stderr. Don't bother copying all that data into notification event.
+        LLProcess::ReadPipe
+            &childout(mChild->getReadPipe(LLProcess::STDOUT)),
+            &childerr(mChild->getReadPipe(LLProcess::STDERR));
+        childout.setLimit(20);
+        childerr.setLimit(20);
+
+        // Serialize any event received on mReplyPump to our child's stdin,
+        // suitably enriched with the pump name on which it was received.
+        mStdinConnection = mReplyPump
+            .listen("LLLeap",
+                    boost::bind(&LLLeapImpl::wstdin, this, mReplyPump.getName(), _1));
+
+        // Listening on stdout is stateful. In general, we're either waiting
+        // for the length prefix or waiting for the specified length of data.
+        // We address that with two different listener methods -- one of which
+        // is blocked at any given time.
+        mStdoutConnection = childout.getPump()
+            .listen("prefix", boost::bind(&LLLeapImpl::rstdout, this, _1));
+        mStdoutDataConnection = childout.getPump()
+            .listen("data",   boost::bind(&LLLeapImpl::rstdoutData, this, _1));
+        mBlocker.reset(new LLEventPump::Blocker(mStdoutDataConnection));
+
+        // Log anything sent up through stderr. When a typical program
+        // encounters an error, it writes its error message to stderr and
+        // terminates with nonzero exit code. In particular, the Python
+        // interpreter behaves that way. More generally, though, a plugin
+        // author can log whatever s/he wants to the viewer log using stderr.
+        mStderrConnection = childerr.getPump()
+            .listen("LLLeap", boost::bind(&LLLeapImpl::rstderr, this, _1));
+
+        // Send child a preliminary event reporting our own reply-pump name --
+        // which would otherwise be pretty tricky to guess!
+// TODO TODO inject name of command pump here.
+        wstdin(mReplyPump.getName(),
+               LLSDMap
+               ("command", LLSD())
+               // Include LLLeap features -- this may be important for child to
+               // construct (or recognize) current protocol.
+               ("features", LLSD::emptyMap()));
+    }
+
+    // Normally we'd expect to arrive here only via done()
+    virtual ~LLLeapImpl()
+    {
+        LL_DEBUGS("LLLeap") << "destroying LLLeap(\"" << mDesc << "\")" << LL_ENDL;
+    }
+
+    // Listener for failed launch attempt
+    bool bad_launch(const LLSD& data)
+    {
+        LL_WARNS("LLLeap") << data["string"].asString() << LL_ENDL;
+        return false;
+    }
+
+    // Listener for child-process termination
+    bool done(const LLSD& data)
+    {
+        // Log the termination
+        LL_INFOS("LLLeap") << data["string"].asString() << LL_ENDL;
+
+        // Any leftover data at this moment are because protocol was not
+        // satisfied. Possibly the child was interrupted in the middle of
+        // sending a message, possibly the child didn't flush stdout before
+        // terminating, possibly it's just garbage. Log its existence but
+        // discard it.
+        LLProcess::ReadPipe& childout(mChild->getReadPipe(LLProcess::STDOUT));
+        if (childout.size())
+        {
+            LLProcess::ReadPipe::size_type
+                peeklen((std::min)(LLProcess::ReadPipe::size_type(50), childout.size()));
+            LL_WARNS("LLLeap") << "Discarding final " << childout.size() << " bytes: "
+                               << childout.peek(0, peeklen) << "..." << LL_ENDL;
+        }
+
+        // Kill this instance. MUST BE LAST before return!
+        delete this;
+        return false;
+    }
+
+    // Listener for events on mReplyPump: send to child stdin
+    bool wstdin(const std::string& pump, const LLSD& data)
+    {
+        LLSD packet(LLSDMap("pump", pump)("data", data));
+
+        std::ostringstream buffer;
+        buffer << LLSDNotationStreamer(packet);
+
+        LL_DEBUGS("EventHost") << "Sending: " << buffer.tellp() << ':';
+        std::string::size_type truncate(80);
+        if (buffer.tellp() <= truncate)
+        {
+            LL_CONT << buffer.str();
+        }
+        else
+        {
+            LL_CONT << buffer.str().substr(0, truncate) << "...";
+        }
+        LL_CONT << LL_ENDL;
+
+        LLProcess::WritePipe& childin(mChild->getWritePipe(LLProcess::STDIN));
+        childin.get_ostream() << buffer.tellp() << ':' << buffer.str() << std::flush;
+        return false;
+    }
+
+    // Initial state of stateful listening on child stdout: wait for a length
+    // prefix, followed by ':'.
+    bool rstdout(const LLSD& data)
+    {
+        LLProcess::ReadPipe& childout(mChild->getReadPipe(LLProcess::STDOUT));
+        // It's possible we got notified of a couple digit characters without
+        // seeing the ':' -- unlikely, but still. Until we see ':', keep
+        // waiting.
+        if (childout.contains(':'))
+        {
+            std::istream& childstream(childout.get_istream());
+            // Saw ':', read length prefix and store in mExpect.
+            size_t expect;
+            childstream >> expect;
+            int colon(childstream.get());
+            if (colon != ':')
+            {
+                // Protocol failure. Clear out the rest of the pending data in
+                // childout (well, up to a max length) to log what was wrong.
+                LLProcess::ReadPipe::size_type
+                    readlen((std::min)(childout.size(), LLProcess::ReadPipe::size_type(80)));
+                std::vector<char> buffer(readlen + 1);
+                childstream.read(&buffer[0], readlen);
+                buffer[childstream.gcount()] = '\0';
+                bad_protocol(STRINGIZE(expect << char(colon) << &buffer[0]));
+            }
+            else
+            {
+                // Saw length prefix, saw colon, life is good. Now wait for
+                // that length of data to arrive.
+                mExpect = expect;
+                // Block calls to this method; resetting mBlocker unblocks
+                // calls to the other method.
+                mBlocker.reset(new LLEventPump::Blocker(mStdoutConnection));
+                // Go check if we've already received all the advertised data.
+                if (childout.size())
+                {
+                    LLSD updata(data);
+                    updata["len"] = LLSD::Integer(childout.size());
+                    rstdoutData(updata);
+                }
+            }
+        }
+        else if (childout.contains('\n'))
+        {
+            // Since this is the initial listening state, this is where we'd
+            // arrive if the child isn't following protocol at all -- say
+            // because the user specified 'ls' or some darn thing.
+            bad_protocol(childout.getline());
+        }
+        return false;
+    }
+
+    // State in which we listen on stdout for the specified length of data to
+    // arrive.
+    bool rstdoutData(const LLSD& data)
+    {
+        LLProcess::ReadPipe& childout(mChild->getReadPipe(LLProcess::STDOUT));
+        // Until we've accumulated the promised length of data, keep waiting.
+        if (childout.size() >= mExpect)
+        {
+            // Ready to rock and roll.
+            LLSD data;
+            LLPointer<LLSDParser> parser(new LLSDNotationParser());
+            S32 parse_status(parser->parse(childout.get_istream(), data, mExpect));
+            if (parse_status == LLSDParser::PARSE_FAILURE)
+            {
+                bad_protocol("unparseable LLSD data");
+            }
+            else if (! (data.isMap() && data["pump"].isString() && data.has("data")))
+            {
+                // we got an LLSD object, but it lacks required keys
+                bad_protocol("missing 'pump' or 'data'");
+            }
+            else
+            {
+                // The LLSD object we got from our stream contains the keys we
+                // need.
+                LLEventPumps::instance().obtain(data["pump"]).post(data["data"]);
+                // Block calls to this method; resetting mBlocker unblocks calls
+                // to the other method.
+                mBlocker.reset(new LLEventPump::Blocker(mStdoutDataConnection));
+                // Go check for any more pending events in the buffer.
+                if (childout.size())
+                {
+                    LLSD updata(data);
+                    data["len"] = LLSD::Integer(childout.size());
+                    rstdout(updata);
+                }
+            }
+        }
+        return false;
+    }
+
+    void bad_protocol(const std::string& data)
+    {
+        LL_WARNS("LLLeap") << mDesc << ": invalid protocol: " << data << LL_ENDL;
+        // No point in continuing to run this child.
+        mChild->kill();
+    }
+
+    // Listen on child stderr and log everything that arrives
+    bool rstderr(const LLSD& data)
+    {
+        LLProcess::ReadPipe& childerr(mChild->getReadPipe(LLProcess::STDERR));
+        // We might have gotten a notification involving only a partial line
+        // -- or multiple lines. Read all complete lines; stop when there's
+        // only a partial line left.
+        while (childerr.contains('\n'))
+        {
+            // DO NOT make calls with side effects in a logging statement! If
+            // that log level is suppressed, your side effects WON'T HAPPEN.
+            std::string line(childerr.getline());
+            // Log the received line. Prefix it with the desc so we know which
+            // plugin it's from. This method name rstderr() is intentionally
+            // chosen to further qualify the log output.
+            LL_INFOS("LLLeap") << mDesc << ": " << line << LL_ENDL;
+        }
+        // What if child writes a final partial line to stderr?
+        if (data["eof"].asBoolean() && childerr.size())
+        {
+            std::string rest(childerr.read(childerr.size()));
+            // Read all remaining bytes and log.
+            LL_INFOS("LLLeap") << mDesc << ": " << rest << LL_ENDL;
+        }
+        return false;
+    }
+
+private:
+    std::string mDesc;
+    LLEventStream mDonePump;
+    LLEventStream mReplyPump;
+    LLProcessPtr mChild;
+    LLTempBoundListener
+        mStdinConnection, mStdoutConnection, mStdoutDataConnection, mStderrConnection;
+    boost::scoped_ptr<LLEventPump::Blocker> mBlocker;
+    LLProcess::ReadPipe::size_type mExpect;
+};
+
+// This must follow the declaration of LLLeapImpl, so it may as well be last.
+LLLeap* LLLeap::create(const std::string& desc, const std::vector<std::string>& plugin, bool exc)
+{
+    // If caller is willing to permit exceptions, just instantiate.
+    if (exc)
+        return new LLLeapImpl(desc, plugin);
+
+    // Caller insists on suppressing LLLeap::Error. Very well, catch it.
+    try
+    {
+        return new LLLeapImpl(desc, plugin);
+    }
+    catch (const LLLeap::Error&)
+    {
+        return NULL;
+    }
+}
+
+LLLeap* LLLeap::create(const std::string& desc, const std::string& plugin, bool exc)
+{
+    // Use LLStringUtil::getTokens() to parse the command line
+    return create(desc,
+                  LLStringUtil::getTokens(plugin,
+                                          " \t\r\n", // drop_delims
+                                          "",        // no keep_delims
+                                          "\"'",     // either kind of quotes
+                                          "\\"),     // backslash escape
+                  exc);
+}
diff --git a/indra/llcommon/llleap.h b/indra/llcommon/llleap.h
new file mode 100644
index 0000000000000000000000000000000000000000..1a1ad23d3925a333aeed1cb57c42c00484df5e80
--- /dev/null
+++ b/indra/llcommon/llleap.h
@@ -0,0 +1,80 @@
+/**
+ * @file   llleap.h
+ * @author Nat Goodspeed
+ * @date   2012-02-20
+ * @brief  Class that implements "LLSD Event API Plugin"
+ * 
+ * $LicenseInfo:firstyear=2012&license=viewerlgpl$
+ * Copyright (c) 2012, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_LLLEAP_H)
+#define LL_LLLEAP_H
+
+#include "llinstancetracker.h"
+#include <string>
+#include <vector>
+#include <stdexcept>
+
+/**
+ * LLSD Event API Plugin class. Because instances are managed by
+ * LLInstanceTracker, you can instantiate LLLeap and forget the instance
+ * unless you need it later. Each instance manages an LLProcess; when the
+ * child process terminates, LLLeap deletes itself. We don't require a unique
+ * LLInstanceTracker key.
+ *
+ * The fact that a given LLLeap instance vanishes when its child process
+ * terminates makes it problematic to store an LLLeap* anywhere. Any stored
+ * LLLeap* pointer should be validated before use by
+ * LLLeap::getInstance(LLLeap*) (see LLInstanceTracker).
+ */
+class LL_COMMON_API LLLeap: public LLInstanceTracker<LLLeap>
+{
+public:
+    /**
+     * Pass a brief string description, mostly for logging purposes. The desc
+     * need not be unique, but obviously the clearer we can make it, the
+     * easier these things will be to debug. The strings are the command line
+     * used to launch the desired plugin process.
+     *
+     * Pass exc=false to suppress LLLeap::Error exception. Obviously in that
+     * case the caller cannot discover the nature of the error, merely that an
+     * error of some kind occurred (because create() returned NULL). Either
+     * way, the error is logged.
+     */
+    static LLLeap* create(const std::string& desc, const std::vector<std::string>& plugin,
+                          bool exc=true);
+
+    /**
+     * Pass a brief string description, mostly for logging purposes. The desc
+     * need not be unique, but obviously the clearer we can make it, the
+     * easier these things will be to debug. Pass a command-line string
+     * to launch the desired plugin process.
+     *
+     * Pass exc=false to suppress LLLeap::Error exception. Obviously in that
+     * case the caller cannot discover the nature of the error, merely that an
+     * error of some kind occurred (because create() returned NULL). Either
+     * way, the error is logged.
+     */
+    static LLLeap* create(const std::string& desc, const std::string& plugin,
+                          bool exc=true);
+
+    /**
+     * Exception thrown for invalid create() arguments, e.g. no plugin
+     * program. This is more resiliant than an LL_ERRS failure, because the
+     * string(s) passed to create() might come from an external source. This
+     * way the caller can catch LLLeap::Error and try to recover.
+     */
+    struct Error: public std::runtime_error
+    {
+        Error(const std::string& what): std::runtime_error(what) {}
+    };
+
+    virtual ~LLLeap();
+
+protected:
+    LLLeap();
+};
+
+#endif /* ! defined(LL_LLLEAP_H) */
diff --git a/indra/llcommon/tests/llleap_test.cpp b/indra/llcommon/tests/llleap_test.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0264442437c1caeea5e9b121964a608842911282
--- /dev/null
+++ b/indra/llcommon/tests/llleap_test.cpp
@@ -0,0 +1,261 @@
+/**
+ * @file   llleap_test.cpp
+ * @author Nat Goodspeed
+ * @date   2012-02-21
+ * @brief  Test for llleap.
+ * 
+ * $LicenseInfo:firstyear=2012&license=viewerlgpl$
+ * Copyright (c) 2012, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "llleap.h"
+// STL headers
+// std headers
+// external library headers
+#include <boost/assign/list_of.hpp>
+#include <boost/lambda/lambda.hpp>
+#include <boost/foreach.hpp>
+// other Linden headers
+#include "../test/lltut.h"
+#include "../test/namedtempfile.h"
+#include "../test/manageapr.h"
+#include "../test/catch_and_store_what_in.h"
+#include "wrapllerrs.h"
+#include "llevents.h"
+#include "llprocess.h"
+#include "stringize.h"
+#include "StringVec.h"
+
+using boost::assign::list_of;
+
+static ManageAPR manager;
+
+StringVec sv(const StringVec& listof) { return listof; }
+
+#if defined(LL_WINDOWS)
+#define sleep(secs) _sleep((secs) * 1000)
+#endif
+
+void waitfor(const std::vector<LLLeap*>& instances)
+{
+    int i, timeout = 60;
+    for (i = 0; i < timeout; ++i)
+    {
+        // Every iteration, test whether any of the passed LLLeap instances
+        // still exist (are still running).
+        std::vector<LLLeap*>::const_iterator vli(instances.begin()), vlend(instances.end());
+        for ( ; vli != vlend; ++vli)
+        {
+            // getInstance() returns NULL if it's terminated/gone, non-NULL if
+            // it's still running
+            if (LLLeap::getInstance(*vli))
+                break;
+        }
+        // If we made it through all of 'instances' without finding one that's
+        // still running, we're done.
+        if (vli == vlend)
+            return;
+        // Found an instance that's still running. Wait and pump LLProcess.
+        sleep(1);
+        LLEventPumps::instance().obtain("mainloop").post(LLSD());
+    }
+    tut::ensure("timed out without terminating", i < timeout);
+}
+
+void waitfor(LLLeap* instance)
+{
+    std::vector<LLLeap*> instances;
+    instances.push_back(instance);
+    waitfor(instances);
+}
+
+/*****************************************************************************
+*   TUT
+*****************************************************************************/
+namespace tut
+{
+    struct llleap_data
+    {
+        llleap_data():
+            reader(".py",
+                   // This logic is adapted from vita.viewerclient.receiveEvent()
+                   "import sys\n"
+                   "LEFTOVER = ''\n"
+                   "class ProtocolError(Exception):\n"
+                   "    pass\n"
+                   "def get():\n"
+                   "    global LEFTOVER\n"
+                   "    hdr = LEFTOVER\n"
+                   "    if ':' not in hdr:\n"
+                   "        hdr += sys.stdin.read(20)\n"
+                   "        if not hdr:\n"
+                   "            sys.exit(0)\n"
+                   "    parts = hdr.split(':', 1)\n"
+                   "    if len(parts) != 2:\n"
+                   "        raise ProtocolError('Expected len:data, got %r' % hdr)\n"
+                   "    try:\n"
+                   "        length = int(parts[0])\n"
+                   "    except ValueError:\n"
+                   "        raise ProtocolError('Non-numeric len %r' % parts[0])\n"
+                   "    del parts[0]\n"
+                   "    received = len(parts[0])\n"
+                   "    while received < length:\n"
+                   "        parts.append(sys.stdin.read(length - received))\n"
+                   "        received += len(parts[-1])\n"
+                   "    if received > length:\n"
+                   "        excess = length - received\n"
+                   "        LEFTOVER = parts[-1][excess:]\n"
+                   "        parts[-1] = parts[-1][:excess]\n"
+                   "    data = ''.join(parts)\n"
+                   "    assert len(data) == length\n"
+                   "    return data\n"),
+            // Get the actual pathname of the NamedExtTempFile and trim off
+            // the ".py" extension. (We could cache reader.getName() in a
+            // separate member variable, but I happen to know getName() just
+            // returns a NamedExtTempFile member rather than performing any
+            // computation, so I don't mind calling it twice.) Then take the
+            // basename.
+            reader_module(LLProcess::basename(
+                              reader.getName().substr(0, reader.getName().length()-3))),
+            pPYTHON(getenv("PYTHON")),
+            PYTHON(pPYTHON? pPYTHON : "")
+        {
+            ensure("Set PYTHON to interpreter pathname", pPYTHON);
+        }
+        NamedExtTempFile reader;
+        const std::string reader_module;
+        const char* pPYTHON;
+        const std::string PYTHON;
+    };
+    typedef test_group<llleap_data> llleap_group;
+    typedef llleap_group::object object;
+    llleap_group llleapgrp("llleap");
+
+    template<> template<>
+    void object::test<1>()
+    {
+        set_test_name("multiple LLLeap instances");
+        NamedTempFile script("py",
+                             "import time\n"
+                             "time.sleep(1)\n");
+        std::vector<LLLeap*> instances;
+        instances.push_back(LLLeap::create(get_test_name(),
+                                           sv(list_of(PYTHON)(script.getName()))));
+        instances.push_back(LLLeap::create(get_test_name(),
+                                           sv(list_of(PYTHON)(script.getName()))));
+        // In this case we're simply establishing that two LLLeap instances
+        // can coexist without throwing exceptions or bombing in any other
+        // way. Wait for them to terminate.
+        waitfor(instances);
+    }
+
+    template<> template<>
+    void object::test<2>()
+    {
+        set_test_name("stderr to log");
+        NamedTempFile script("py",
+                             "import sys\n"
+                             "sys.stderr.write('''Hello from Python!\n"
+                             "note partial line''')\n");
+        CaptureLog log(LLError::LEVEL_INFO);
+        waitfor(LLLeap::create(get_test_name(),
+                               sv(list_of(PYTHON)(script.getName()))));
+        log.messageWith("Hello from Python!");
+        log.messageWith("note partial line");
+    }
+
+    template<> template<>
+    void object::test<3>()
+    {
+        set_test_name("empty plugin vector");
+        std::string threw;
+        try
+        {
+            LLLeap::create("empty", StringVec());
+        }
+        CATCH_AND_STORE_WHAT_IN(threw, LLLeap::Error)
+        ensure_contains("LLLeap::Error", threw, "no plugin");
+        // try the suppress-exception variant
+        ensure("bad launch returned non-NULL", ! LLLeap::create("empty", StringVec(), false));
+    }
+
+    template<> template<>
+    void object::test<4>()
+    {
+        set_test_name("bad launch");
+        // Synthesize bogus executable name
+        std::string BADPYTHON(PYTHON.substr(0, PYTHON.length()-1) + "x");
+        CaptureLog log;
+        std::string threw;
+        try
+        {
+            LLLeap::create("bad exe", BADPYTHON);
+        }
+        CATCH_AND_STORE_WHAT_IN(threw, LLLeap::Error)
+        ensure_contains("LLLeap::create() didn't throw", threw, "failed");
+        log.messageWith("failed");
+        log.messageWith(BADPYTHON);
+        // try the suppress-exception variant
+        ensure("bad launch returned non-NULL", ! LLLeap::create("bad exe", BADPYTHON, false));
+    }
+
+    // Mimic a dummy little LLEventAPI that merely sends a reply back to its
+    // requester on the "reply" pump.
+    struct API
+    {
+        API():
+            mPump("API", true)
+        {
+            mPump.listen("API", boost::bind(&API::entry, this, _1));
+        }
+
+        bool entry(const LLSD& request)
+        {
+            LLEventPumps::instance().obtain(request["reply"]).post("ack");
+            return false;
+        }
+
+        LLEventStream mPump;
+    };
+
+    template<> template<>
+    void object::test<5>()
+    {
+        set_test_name("round trip");
+        API api;
+        NamedTempFile script("py",
+                             boost::lambda::_1 <<
+                             "import re\n"
+                             "import sys\n"
+                             "from " << reader_module << " import get\n"
+                             // this will throw if the initial write to stdin
+                             // doesn't follow len:data protocol
+                             "initial = get()\n"
+                             "match = re.search(r\"'pump':'(.*?)'\", initial)\n"
+                             // this will throw if we couldn't find
+                             // 'pump':'etc.' in the initial write
+                             "reply = match.group(1)\n"
+                             "req = '''\\\n"
+                             "{'pump':'" << api.mPump.getName() << "','data':{'reply':'%s'}}\\\n"
+                             "''' % reply\n"
+                             // make a request on our little API
+                             "sys.stdout.write(':'.join((str(len(req)), req)))\n"
+                             "sys.stdout.flush()\n"
+                             // wait for its response
+                             "resp = get()\n"
+                             // it would be cleverer to be order-insensitive
+                             // about 'data' and 'pump'; hopefully the C++
+                             // serializer doesn't change its rules soon
+                             "result = 'good' if (resp == \"{'data':'ack','pump':'%s'}\" % reply)\\\n"
+                             "                else 'bad: ' + resp\n"
+                             // write 'good' or 'bad' to the log so we can observe
+                             "sys.stderr.write(result)\n");
+        CaptureLog log(LLError::LEVEL_INFO);
+        waitfor(LLLeap::create(get_test_name(), sv(list_of(PYTHON)(script.getName()))));
+        log.messageWith("good");
+    }
+} // namespace tut