diff options
-rw-r--r-- | indra/llcommon/CMakeLists.txt | 3 | ||||
-rw-r--r-- | indra/llcommon/llleap.cpp | 379 | ||||
-rw-r--r-- | indra/llcommon/llleap.h | 80 | ||||
-rw-r--r-- | indra/llcommon/tests/llleap_test.cpp | 261 |
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 |