summaryrefslogtreecommitdiff
path: root/indra
diff options
context:
space:
mode:
authorNat Goodspeed <nat@lindenlab.com>2012-03-01 17:46:44 -0500
committerNat Goodspeed <nat@lindenlab.com>2012-03-01 17:46:44 -0500
commit0ef99cd33b4b450712e105e8ab448c2dab7081d2 (patch)
tree78fafe7dc94b10d3183fb068ba63cd9f44ae9b27 /indra
parentf0612f6fc424ab4726ef187f41a257c0abdd1d34 (diff)
Add LLLeap class, initial implementation, initial unit tests.
Instantiating LLLeap with a command to execute a particular child process sets up machinery to speak LLSD Event API Plugin protocol with that child process. LLLeap is an LLInstanceTracker subclass, so the code that instantiates need not hold the pointer. LLLeap monitors child-process termination and deletes itself when done.
Diffstat (limited to 'indra')
-rw-r--r--indra/llcommon/CMakeLists.txt3
-rw-r--r--indra/llcommon/llleap.cpp379
-rw-r--r--indra/llcommon/llleap.h80
-rw-r--r--indra/llcommon/tests/llleap_test.cpp261
4 files changed, 723 insertions, 0 deletions
diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt
index 1cab648cfa..47a8aa96aa 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 0000000000..dddf1286ac
--- /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 0000000000..1a1ad23d39
--- /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 0000000000..0264442437
--- /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