diff options
Diffstat (limited to 'indra/llcommon')
35 files changed, 6077 insertions, 884 deletions
| diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index f9bc22e98a..dd7b8c6eb8 100644 --- a/indra/llcommon/CMakeLists.txt +++ b/indra/llcommon/CMakeLists.txt @@ -63,6 +63,8 @@ set(llcommon_SOURCE_FILES      llheartbeat.cpp      llinitparam.cpp      llinstancetracker.cpp +    llleap.cpp +    llleaplistener.cpp      llliveappconfig.cpp      lllivefile.cpp      lllog.cpp @@ -75,7 +77,7 @@ set(llcommon_SOURCE_FILES      llmortician.cpp      lloptioninterface.cpp      llptrto.cpp  -    llprocesslauncher.cpp +    llprocess.cpp      llprocessor.cpp      llqueuedthread.cpp      llrand.cpp @@ -90,6 +92,7 @@ set(llcommon_SOURCE_FILES      llsingleton.cpp      llstat.cpp      llstacktrace.cpp +    llstreamqueue.cpp      llstreamtools.cpp      llstring.cpp      llstringtable.cpp @@ -179,6 +182,8 @@ set(llcommon_HEADER_FILES      llinstancetracker.h      llkeythrottle.h      lllazy.h +    llleap.h +    llleaplistener.h      lllistenerwrapper.h      lllinkedqueue.h      llliveappconfig.h @@ -199,7 +204,7 @@ set(llcommon_HEADER_FILES      llpointer.h      llpreprocessor.h      llpriqueuemap.h -    llprocesslauncher.h +    llprocess.h      llprocessor.h      llptrskiplist.h      llptrskipmap.h @@ -227,6 +232,7 @@ set(llcommon_HEADER_FILES      llstat.h      llstatenums.h      llstl.h +    llstreamqueue.h      llstreamtools.h      llstrider.h      llstring.h @@ -324,8 +330,7 @@ if (LL_TESTS)    LL_ADD_INTEGRATION_TEST(lllazy "" "${test_libs}")    LL_ADD_INTEGRATION_TEST(llprocessor "" "${test_libs}")    LL_ADD_INTEGRATION_TEST(llrand "" "${test_libs}") -  LL_ADD_INTEGRATION_TEST(llsdserialize "" "${test_libs}" -                          "${PYTHON_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/setpython.py") +  LL_ADD_INTEGRATION_TEST(llsdserialize "" "${test_libs}")    LL_ADD_INTEGRATION_TEST(llsingleton "" "${test_libs}")                              LL_ADD_INTEGRATION_TEST(llstring "" "${test_libs}")    LL_ADD_INTEGRATION_TEST(lltreeiterators "" "${test_libs}") @@ -333,6 +338,9 @@ if (LL_TESTS)    LL_ADD_INTEGRATION_TEST(reflection "" "${test_libs}")    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.    #ADD_BUILD_TEST(llallocator llcommon) diff --git a/indra/llcommon/llerror.cpp b/indra/llcommon/llerror.cpp index c35799bbb9..7e6eee0f3c 100644 --- a/indra/llcommon/llerror.cpp +++ b/indra/llcommon/llerror.cpp @@ -617,6 +617,12 @@ namespace LLError  		s.defaultLevel = level;  	} +	ELevel getDefaultLevel() +	{ +		Settings& s = Settings::get(); +		return s.defaultLevel; +	} +  	void setFunctionLevel(const std::string& function_name, ELevel level)  	{  		Globals& g = Globals::get(); @@ -648,9 +654,7 @@ namespace LLError  		g.invalidateCallSites();  		s.tagLevelMap[tag_name] = level;  	} -} -namespace {  	LLError::ELevel decodeLevel(std::string name)  	{  		static LevelMap level_names; @@ -675,7 +679,9 @@ namespace {  		return i->second;  	} -	 +} + +namespace {  	void setLevels(LevelMap& map, const LLSD& list, LLError::ELevel level)  	{  		LLSD::array_const_iterator i, end; diff --git a/indra/llcommon/llerrorcontrol.h b/indra/llcommon/llerrorcontrol.h index fb75d45e2c..1be49cebc8 100644 --- a/indra/llcommon/llerrorcontrol.h +++ b/indra/llcommon/llerrorcontrol.h @@ -75,11 +75,13 @@ namespace LLError  	LL_COMMON_API void setPrintLocation(bool);  	LL_COMMON_API void setDefaultLevel(LLError::ELevel); +	LL_COMMON_API ELevel getDefaultLevel();  	LL_COMMON_API void setFunctionLevel(const std::string& function_name, LLError::ELevel);  	LL_COMMON_API void setClassLevel(const std::string& class_name, LLError::ELevel);  	LL_COMMON_API void setFileLevel(const std::string& file_name, LLError::ELevel);  	LL_COMMON_API void setTagLevel(const std::string& file_name, LLError::ELevel); -	 + +	LL_COMMON_API LLError::ELevel decodeLevel(std::string name);  	LL_COMMON_API void configure(const LLSD&);  		// the LLSD can configure all of the settings  		// usually read automatically from the live errorlog.xml file diff --git a/indra/llcommon/llerrorthread.cpp b/indra/llcommon/llerrorthread.cpp index 902eaa3b72..950fcd6e83 100644 --- a/indra/llcommon/llerrorthread.cpp +++ b/indra/llcommon/llerrorthread.cpp @@ -112,13 +112,8 @@ void LLErrorThread::run()  #if !LL_WINDOWS  	U32 last_sig_child_count = 0;  #endif -	while (1) +	while (! (LLApp::isError() || LLApp::isStopped()))  	{ -		if (LLApp::isError() || LLApp::isStopped()) -		{ -			// The application has stopped running, time to take action (maybe) -			break; -		}  #if !LL_WINDOWS  		// Check whether or not the main thread had a sig child we haven't handled.  		U32 current_sig_child_count = LLApp::getSigChildCount(); diff --git a/indra/llcommon/llinstancetracker.h b/indra/llcommon/llinstancetracker.h index 34d841a4e0..403df08990 100644 --- a/indra/llcommon/llinstancetracker.h +++ b/indra/llcommon/llinstancetracker.h @@ -167,8 +167,9 @@ public:  	static T* getInstance(const KEY& k)  	{ -		typename InstanceMap::const_iterator found = getMap_().find(k); -		return (found == getMap_().end()) ? NULL : found->second; +		const InstanceMap& map(getMap_()); +		typename InstanceMap::const_iterator found = map.find(k); +		return (found == map.end()) ? NULL : found->second;  	}  	static instance_iter beginInstances()  @@ -239,8 +240,20 @@ class LLInstanceTracker<T, T*> : public LLInstanceTrackerBase  public: -	/// for completeness of analogy with the generic implementation -	static T* getInstance(T* k) { return k; } +	/** +	 * Does a particular instance still exist? Of course, if you already have +	 * a T* in hand, you need not call getInstance() to @em locate the +	 * instance -- unlike the case where getInstance() accepts some kind of +	 * key. Nonetheless this method is still useful to @em validate a +	 * particular T*, since each instance's destructor removes itself from the +	 * underlying set. +	 */ +	static T* getInstance(T* k) +	{ +		const InstanceSet& set(getSet_()); +		typename InstanceSet::const_iterator found = set.find(k); +		return (found == set.end())? NULL : *found; +	}  	static S32 instanceCount() { return getSet_().size(); }  	class instance_iter : public boost::iterator_facade<instance_iter, T, boost::forward_traversal_tag> diff --git a/indra/llcommon/llleap.cpp b/indra/llcommon/llleap.cpp new file mode 100644 index 0000000000..0a57ef1c48 --- /dev/null +++ b/indra/llcommon/llleap.cpp @@ -0,0 +1,459 @@ +/** + * @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" +#include "llerrorcontrol.h" +#include "lltimer.h" +#include "lluuid.h" +#include "llleaplistener.h" + +#if LL_MSVC +#pragma warning (disable : 4355) // 'this' used in initializer list: yes, intentionally +#endif + +LLLeap::LLLeap() {} +LLLeap::~LLLeap() {} + +class LLLeapImpl: public LLLeap +{ +    LOG_CLASS(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), +        mPrevFatalFunction(LLError::getFatalFunction()), +        // Instantiate a distinct LLLeapListener for this plugin. (Every +        // plugin will want its own collection of managed listeners, etc.) +        // Pass it a callback to our connect() method, so it can send events +        // from a particular LLEventPump to the plugin without having to know +        // this class or method name. +        mListener(new LLLeapListener(boost::bind(&LLLeapImpl::connect, this, _1, _2))) +    { +        // 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. +        mStdinConnection = connect(mReplyPump, "LLLeap"); + +        // 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)); + +        // For our lifespan, intercept any LL_ERRS so we can notify plugin +        LLError::setFatalFunction(boost::bind(&LLLeapImpl::fatalFunction, this, _1)); + +        // Send child a preliminary event reporting our own reply-pump name -- +        // which would otherwise be pretty tricky to guess! +        wstdin(mReplyPump.getName(), +               LLSDMap +               ("command", mListener->getName()) +               // Include LLLeap features -- this may be important for child to +               // construct (or recognize) current protocol. +               ("features", LLLeapListener::getFeatures())); +    } + +    // Normally we'd expect to arrive here only via done() +    virtual ~LLLeapImpl() +    { +        LL_DEBUGS("LLLeap") << "destroying LLLeap(\"" << mDesc << "\")" << LL_ENDL; +        // Restore original FatalFunction +        LLError::setFatalFunction(mPrevFatalFunction); +    } + +    // 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); + +/*==========================================================================*| +        // DEBUGGING ONLY: don't copy str() if we can avoid it. +        std::string strdata(buffer.str()); +        if (std::size_t(buffer.tellp()) != strdata.length()) +        { +            LL_ERRS("LLLeap") << "tellp() -> " << buffer.tellp() << " != " +                              << "str().length() -> " << strdata.length() << LL_ENDL; +        } +        // DEBUGGING ONLY: reading back is terribly inefficient. +        std::istringstream readback(strdata); +        LLSD echo; +        LLPointer<LLSDParser> parser(new LLSDNotationParser()); +        S32 parse_status(parser->parse(readback, echo, strdata.length())); +        if (parse_status == LLSDParser::PARSE_FAILURE) +        { +            LL_ERRS("LLLeap") << "LLSDNotationParser() cannot parse output of " +                              << "LLSDNotationStreamer()" << LL_ENDL; +        } +        if (! llsd_equals(echo, packet)) +        { +            LL_ERRS("LLLeap") << "LLSDNotationParser() produced different LLSD " +                              << "than passed to LLSDNotationStreamer()" << LL_ENDL; +        } +|*==========================================================================*/ + +        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))); +                bad_protocol(STRINGIZE(expect << char(colon) << childout.read(readlen))); +            } +            else +            { +                // Saw length prefix, saw colon, life is good. Now wait for +                // that length of data to arrive. +                mExpect = expect; +                LL_DEBUGS("LLLeap") << "got length, waiting for " +                                    << mExpect << " bytes of data" << LL_ENDL; +                // 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. +            LL_DEBUGS("LLLeap") << "needed " << mExpect << " bytes, got " +                                << childout.size() << ", parsing LLSD" << LL_ENDL; +            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; +    } + +    void fatalFunction(const std::string& error) +    { +        // Notify plugin +        LLSD event; +        event["type"] = "error"; +        event["error"] = error; +        mReplyPump.post(event); + +        // All the above really accomplished was to buffer the serialized +        // event in our WritePipe. Have to pump mainloop a couple times to +        // really write it out there... but time out in case we can't write. +        LLProcess::WritePipe& childin(mChild->getWritePipe(LLProcess::STDIN)); +        LLEventPump& mainloop(LLEventPumps::instance().obtain("mainloop")); +        LLSD nop; +        F64 until(LLTimer::getElapsedSeconds() + 2); +        while (childin.size() && LLTimer::getElapsedSeconds() < until) +        { +            mainloop.post(nop); +        } + +        // forward the call to the previous FatalFunction +        mPrevFatalFunction(error); +    } + +private: +    /// We always want to listen on mReplyPump with wstdin(); under some +    /// circumstances we'll also echo other LLEventPumps to the plugin. +    LLBoundListener connect(LLEventPump& pump, const std::string& listener) +    { +        // Serialize any event received on the specified LLEventPump to our +        // child's stdin, suitably enriched with the pump name on which it was +        // received. +        return pump.listen(listener, +                           boost::bind(&LLLeapImpl::wstdin, this, pump.getName(), _1)); +    } + +    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; +    LLError::FatalFunction mPrevFatalFunction; +    boost::scoped_ptr<LLLeapListener> mListener; +}; + +// 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/llleaplistener.cpp b/indra/llcommon/llleaplistener.cpp new file mode 100644 index 0000000000..fa5730f112 --- /dev/null +++ b/indra/llcommon/llleaplistener.cpp @@ -0,0 +1,287 @@ +/** + * @file   llleaplistener.cpp + * @author Nat Goodspeed + * @date   2012-03-16 + * @brief  Implementation for llleaplistener. + *  + * $LicenseInfo:firstyear=2012&license=viewerlgpl$ + * Copyright (c) 2012, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "llleaplistener.h" +// STL headers +// std headers +// external library headers +#include <boost/foreach.hpp> +// other Linden headers +#include "lluuid.h" +#include "llsdutil.h" +#include "stringize.h" + +/***************************************************************************** +*   LEAP FEATURE STRINGS +*****************************************************************************/ +/** + * Implement "getFeatures" command. The LLSD map thus obtained is intended to + * be machine-readable (read: easily-parsed, if parsing be necessary) and to + * highlight the differences between this version of the LEAP protocol and + * the baseline version. A client may thus determine whether or not the + * running viewer supports some recent feature of interest. + * + * This method is defined at the top of this implementation file so it's easy + * to find, easy to spot, easy to update as we enhance the LEAP protocol. + */ +/*static*/ LLSD LLLeapListener::getFeatures() +{ +    static LLSD features; +    if (features.isUndefined()) +    { +        features = LLSD::emptyMap(); + +        // This initial implementation IS the baseline LEAP protocol; thus the +        // set of differences is empty; thus features is initially empty. +//      features["featurename"] = "value"; +    } + +    return features; +} + +LLLeapListener::LLLeapListener(const ConnectFunc& connect): +    // Each LEAP plugin has an instance of this listener. Make the command +    // pump name difficult for other such plugins to guess. +    LLEventAPI(LLUUID::generateNewID().asString(), +               "Operations relating to the LLSD Event API Plugin (LEAP) protocol"), +    mConnect(connect) +{ +    LLSD need_name(LLSDMap("name", LLSD())); +    add("newpump", +        "Instantiate a new LLEventPump named like [\"name\"] and listen to it.\n" +        "If [\"type\"] == \"LLEventQueue\", make LLEventQueue, else LLEventStream.\n" +        "Events sent through new LLEventPump will be decorated with [\"pump\"]=name.\n" +        "Returns actual name in [\"name\"] (may be different if collision).", +        &LLLeapListener::newpump, +        need_name); +    add("killpump", +        "Delete LLEventPump [\"name\"] created by \"newpump\".\n" +        "Returns [\"status\"] boolean indicating whether such a pump existed.", +        &LLLeapListener::killpump, +        need_name); +    LLSD need_source_listener(LLSDMap("source", LLSD())("listener", LLSD())); +    add("listen", +        "Listen to an existing LLEventPump named [\"source\"], with listener name\n" +        "[\"listener\"].\n" +        "By default, send events on [\"source\"] to the plugin, decorated\n" +        "with [\"pump\"]=[\"source\"].\n" +        "If [\"dest\"] specified, send undecorated events on [\"source\"] to the\n" +        "LLEventPump named [\"dest\"].\n" +        "Returns [\"status\"] boolean indicating whether the connection was made.", +        &LLLeapListener::listen, +        need_source_listener); +    add("stoplistening", +        "Disconnect a connection previously established by \"listen\".\n" +        "Pass same [\"source\"] and [\"listener\"] arguments.\n" +        "Returns [\"status\"] boolean indicating whether such a listener existed.", +        &LLLeapListener::stoplistening, +        need_source_listener); +    add("ping", +        "No arguments, just a round-trip sanity check.", +        &LLLeapListener::ping); +    add("getAPIs", +        "Enumerate all LLEventAPI instances by name and description.", +        &LLLeapListener::getAPIs); +    add("getAPI", +        "Get name, description, dispatch key and operations for LLEventAPI [\"api\"].", +        &LLLeapListener::getAPI, +        LLSD().with("api", LLSD())); +    add("getFeatures", +        "Return an LLSD map of feature strings (deltas from baseline LEAP protocol)", +        static_cast<void (LLLeapListener::*)(const LLSD&) const>(&LLLeapListener::getFeatures)); +    add("getFeature", +        "Return the feature value with key [\"feature\"]", +        &LLLeapListener::getFeature, +        LLSD().with("feature", LLSD())); +} + +LLLeapListener::~LLLeapListener() +{ +    // We'd have stored a map of LLTempBoundListener instances, save that the +    // operation of inserting into a std::map necessarily copies the +    // value_type, and Bad Things would happen if you copied an +    // LLTempBoundListener. (Destruction of the original would disconnect the +    // listener, invalidating every stored connection.) +    BOOST_FOREACH(ListenersMap::value_type& pair, mListeners) +    { +        pair.second.disconnect(); +    } +} + +void LLLeapListener::newpump(const LLSD& request) +{ +    Response reply(LLSD(), request); + +    std::string name = request["name"]; +    LLSD const & type = request["type"]; + +    LLEventPump * new_pump = NULL; +    if (type.asString() == "LLEventQueue") +    { +        new_pump = new LLEventQueue(name, true); // tweak name for uniqueness +    } +    else +    { +        if (! (type.isUndefined() || type.asString() == "LLEventStream")) +        { +            reply.warn(STRINGIZE("unknown 'type' " << type << ", using LLEventStream")); +        } +        new_pump = new LLEventStream(name, true); // tweak name for uniqueness +    } + +    name = new_pump->getName(); + +    mEventPumps.insert(name, new_pump); + +    // Now listen on this new pump with our plugin listener +    std::string myname("llleap"); +    saveListener(name, myname, mConnect(*new_pump, myname)); + +    reply["name"] = name; +} + +void LLLeapListener::killpump(const LLSD& request) +{ +    Response reply(LLSD(), request); + +    std::string name = request["name"]; +    // success == (nonzero number of entries were erased) +    reply["status"] = bool(mEventPumps.erase(name)); +} + +void LLLeapListener::listen(const LLSD& request) +{ +    Response reply(LLSD(), request); + +    std::string source_name = request["source"]; +    std::string dest_name = request["dest"]; +    std::string listener_name = request["listener"]; + +    LLEventPump & source = LLEventPumps::instance().obtain(source_name); + +    reply["status"] = false; +    if (mListeners.find(ListenersMap::key_type(source_name, listener_name)) == mListeners.end()) +    { +        try +        { +            if (request["dest"].isDefined()) +            { +                // If we're asked to connect the "source" pump to a +                // specific "dest" pump, find dest pump and connect it. +                LLEventPump & dest = LLEventPumps::instance().obtain(dest_name); +                saveListener(source_name, listener_name, +                             source.listen(listener_name, +                                           boost::bind(&LLEventPump::post, &dest, _1))); +            } +            else +            { +                // "dest" unspecified means to direct events on "source" +                // to our plugin listener. +                saveListener(source_name, listener_name, mConnect(source, listener_name)); +            } +            reply["status"] = true; +        } +        catch (const LLEventPump::DupListenerName &) +        { +            // pass - status already set to false +        } +    } +} + +void LLLeapListener::stoplistening(const LLSD& request) +{ +    Response reply(LLSD(), request); + +    std::string source_name = request["source"]; +    std::string listener_name = request["listener"]; + +    ListenersMap::iterator finder = +        mListeners.find(ListenersMap::key_type(source_name, listener_name)); + +    reply["status"] = false; +    if(finder != mListeners.end()) +    { +        reply["status"] = true; +        finder->second.disconnect(); +        mListeners.erase(finder); +    } +} + +void LLLeapListener::ping(const LLSD& request) const +{ +    // do nothing, default reply suffices +    Response(LLSD(), request); +} + +void LLLeapListener::getAPIs(const LLSD& request) const +{ +    Response reply(LLSD(), request); + +    for (LLEventAPI::instance_iter eai(LLEventAPI::beginInstances()), +             eaend(LLEventAPI::endInstances()); +         eai != eaend; ++eai) +    { +        LLSD info; +        info["desc"] = eai->getDesc(); +        reply[eai->getName()] = info; +    } +} + +void LLLeapListener::getAPI(const LLSD& request) const +{ +    Response reply(LLSD(), request); + +    LLEventAPI* found = LLEventAPI::getInstance(request["api"]); +    if (found) +    { +        reply["name"] = found->getName(); +        reply["desc"] = found->getDesc(); +        reply["key"] = found->getDispatchKey(); +        LLSD ops; +        for (LLEventAPI::const_iterator oi(found->begin()), oend(found->end()); +             oi != oend; ++oi) +        { +            ops.append(found->getMetadata(oi->first)); +        } +        reply["ops"] = ops; +    } +} + +void LLLeapListener::getFeatures(const LLSD& request) const +{ +    // Merely constructing and destroying a Response object suffices here. +    // Giving it a name would only produce fatal 'unreferenced variable' +    // warnings. +    Response(getFeatures(), request); +} + +void LLLeapListener::getFeature(const LLSD& request) const +{ +    Response reply(LLSD(), request); + +    LLSD::String feature_name(request["feature"]); +    LLSD features(getFeatures()); +    if (features[feature_name].isDefined()) +    { +        reply["feature"] = features[feature_name]; +    } +} + +void LLLeapListener::saveListener(const std::string& pump_name, +                                  const std::string& listener_name, +                                  const LLBoundListener& listener) +{ +    mListeners.insert(ListenersMap::value_type(ListenersMap::key_type(pump_name, listener_name), +                                               listener)); +} diff --git a/indra/llcommon/llleaplistener.h b/indra/llcommon/llleaplistener.h new file mode 100644 index 0000000000..2193d81b9e --- /dev/null +++ b/indra/llcommon/llleaplistener.h @@ -0,0 +1,73 @@ +/** + * @file   llleaplistener.h + * @author Nat Goodspeed + * @date   2012-03-16 + * @brief  LLEventAPI supporting LEAP plugins + *  + * $LicenseInfo:firstyear=2012&license=viewerlgpl$ + * Copyright (c) 2012, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_LLLEAPLISTENER_H) +#define LL_LLLEAPLISTENER_H + +#include "lleventapi.h" +#include <map> +#include <string> +#include <boost/function.hpp> +#include <boost/ptr_container/ptr_map.hpp> + +/// Listener class implementing LLLeap query/control operations. +/// See https://jira.lindenlab.com/jira/browse/DEV-31978. +class LLLeapListener: public LLEventAPI +{ +public: +    /** +     * Decouple LLLeap by dependency injection. Certain LLLeapListener +     * operations must be able to cause LLLeap to listen on a specified +     * LLEventPump with the LLLeap listener that wraps incoming events in an +     * outer (pump=, data=) map and forwards them to the plugin. Very well, +     * define the signature for a function that will perform that, and make +     * our constructor accept such a function. +     */ +    typedef boost::function<LLBoundListener(LLEventPump&, const std::string& listener)> +            ConnectFunc; +    LLLeapListener(const ConnectFunc& connect); +    ~LLLeapListener(); + +    static LLSD getFeatures(); + +private: +    void newpump(const LLSD&); +    void killpump(const LLSD&); +    void listen(const LLSD&); +    void stoplistening(const LLSD&); +    void ping(const LLSD&) const; +    void getAPIs(const LLSD&) const; +    void getAPI(const LLSD&) const; +    void getFeatures(const LLSD&) const; +    void getFeature(const LLSD&) const; + +    void saveListener(const std::string& pump_name, const std::string& listener_name, +                      const LLBoundListener& listener); + +    ConnectFunc mConnect; + +    // In theory, listen() could simply call the relevant LLEventPump's +    // listen() method, stoplistening() likewise. Lifespan issues make us +    // capture the LLBoundListener objects: when this object goes away, all +    // those listeners should be disconnected. But what if the client listens, +    // stops, listens again on the same LLEventPump with the same listener +    // name? Merely collecting LLBoundListeners wouldn't adequately track +    // that. So capture the latest LLBoundListener for this LLEventPump name +    // and listener name. +    typedef std::map<std::pair<std::string, std::string>, LLBoundListener> ListenersMap; +    ListenersMap mListeners; +    // Similar lifespan reasoning applies to LLEventPumps instantiated by +    // newpump() operations. +    typedef boost::ptr_map<std::string, LLEventPump> EventPumpsMap; +    EventPumpsMap mEventPumps; +}; + +#endif /* ! defined(LL_LLLEAPLISTENER_H) */ diff --git a/indra/llcommon/llprocess.cpp b/indra/llcommon/llprocess.cpp new file mode 100644 index 0000000000..d4786035ce --- /dev/null +++ b/indra/llcommon/llprocess.cpp @@ -0,0 +1,1295 @@ +/**  + * @file llprocess.cpp + * @brief Utility class for launching, terminating, and tracking the state of processes. + * + * $LicenseInfo:firstyear=2008&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, Linden Research, Inc. + *  + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + *  + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU + * Lesser General Public License for more details. + *  + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA + *  + * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" +#include "llprocess.h" +#include "llsdutil.h" +#include "llsdserialize.h" +#include "llsingleton.h" +#include "llstring.h" +#include "stringize.h" +#include "llapr.h" +#include "apr_signal.h" +#include "llevents.h" + +#include <boost/foreach.hpp> +#include <boost/bind.hpp> +#include <boost/asio/streambuf.hpp> +#include <boost/asio/buffers_iterator.hpp> +#include <iostream> +#include <stdexcept> +#include <limits> +#include <algorithm> +#include <vector> +#include <typeinfo> +#include <utility> + +/***************************************************************************** +*   Helpers +*****************************************************************************/ +static const char* whichfile_[] = { "stdin", "stdout", "stderr" }; +static std::string empty; +static LLProcess::Status interpret_status(int status); +static std::string getDesc(const LLProcess::Params& params); + +static std::string whichfile(LLProcess::FILESLOT index) +{ +	if (index < LL_ARRAY_SIZE(whichfile_)) +		return whichfile_[index]; +	return STRINGIZE("file slot " << index); +} + +/** + * Ref-counted "mainloop" listener. As long as there are still outstanding + * LLProcess objects, keep listening on "mainloop" so we can keep polling APR + * for process status. + */ +class LLProcessListener +{ +	LOG_CLASS(LLProcessListener); +public: +	LLProcessListener(): +		mCount(0) +	{} + +	void addPoll(const LLProcess&) +	{ +		// Unconditionally increment mCount. If it was zero before +		// incrementing, listen on "mainloop". +		if (mCount++ == 0) +		{ +			LL_DEBUGS("LLProcess") << "listening on \"mainloop\"" << LL_ENDL; +			mConnection = LLEventPumps::instance().obtain("mainloop") +				.listen("LLProcessListener", boost::bind(&LLProcessListener::tick, this, _1)); +		} +	} + +	void dropPoll(const LLProcess&) +	{ +		// Unconditionally decrement mCount. If it's zero after decrementing, +		// stop listening on "mainloop". +		if (--mCount == 0) +		{ +			LL_DEBUGS("LLProcess") << "disconnecting from \"mainloop\"" << LL_ENDL; +			mConnection.disconnect(); +		} +	} + +private: +	/// called once per frame by the "mainloop" LLEventPump +	bool tick(const LLSD&) +	{ +		// Tell APR to sense whether each registered LLProcess is still +		// running and call handle_status() appropriately. We should be able +		// to get the same info from an apr_proc_wait(APR_NOWAIT) call; but at +		// least in APR 1.4.2, testing suggests that even with APR_NOWAIT, +		// apr_proc_wait() blocks the caller. We can't have that in the +		// viewer. Hence the callback rigmarole. (Once we update APR, it's +		// probably worth testing again.) Also -- although there's an +		// apr_proc_other_child_refresh() call, i.e. get that information for +		// one specific child, it accepts an 'apr_other_child_rec_t*' that's +		// mentioned NOWHERE else in the documentation or header files! I +		// would use the specific call in LLProcess::getStatus() if I knew +		// how. As it is, each call to apr_proc_other_child_refresh_all() will +		// call callbacks for ALL still-running child processes. That's why we +		// centralize such calls, using "mainloop" to ensure it happens once +		// per frame, and refcounting running LLProcess objects to remain +		// registered only while needed. +		LL_DEBUGS("LLProcess") << "calling apr_proc_other_child_refresh_all()" << LL_ENDL; +		apr_proc_other_child_refresh_all(APR_OC_REASON_RUNNING); +		return false; +	} + +	/// If this object is destroyed before mCount goes to zero, stop +	/// listening on "mainloop" anyway. +	LLTempBoundListener mConnection; +	unsigned mCount; +}; +static LLProcessListener sProcessListener; + +/***************************************************************************** +*   WritePipe and ReadPipe +*****************************************************************************/ +LLProcess::BasePipe::~BasePipe() {} +const LLProcess::BasePipe::size_type +	  // use funky syntax to call max() to avoid blighted max() macros +	  LLProcess::BasePipe::npos((std::numeric_limits<LLProcess::BasePipe::size_type>::max)()); + +class WritePipeImpl: public LLProcess::WritePipe +{ +	LOG_CLASS(WritePipeImpl); +public: +	WritePipeImpl(const std::string& desc, apr_file_t* pipe): +		mDesc(desc), +		mPipe(pipe), +		// Essential to initialize our std::ostream with our special streambuf! +		mStream(&mStreambuf) +	{ +		mConnection = LLEventPumps::instance().obtain("mainloop") +			.listen(LLEventPump::inventName("WritePipe"), +					boost::bind(&WritePipeImpl::tick, this, _1)); + +#if ! LL_WINDOWS +		// We can't count on every child process reading everything we try to +		// write to it. And if the child terminates with WritePipe data still +		// pending, unless we explicitly suppress it, Posix will hit us with +		// SIGPIPE. That would terminate the viewer, boom. "Ignoring" it means +		// APR gets the correct errno, passes it back to us, we log it, etc. +		signal(SIGPIPE, SIG_IGN); +#endif +	} + +	virtual std::ostream& get_ostream() { return mStream; } +	virtual size_type size() const { return mStreambuf.size(); } + +	bool tick(const LLSD&) +	{ +		typedef boost::asio::streambuf::const_buffers_type const_buffer_sequence; +		// If there's anything to send, try to send it. +		std::size_t total(mStreambuf.size()), consumed(0); +		if (total) +		{ +			const_buffer_sequence bufs = mStreambuf.data(); +			// In general, our streambuf might contain a number of different +			// physical buffers; iterate over those. +			bool keepwriting = true; +			for (const_buffer_sequence::const_iterator bufi(bufs.begin()), bufend(bufs.end()); +				 bufi != bufend && keepwriting; ++bufi) +			{ +				// http://www.boost.org/doc/libs/1_49_0_beta1/doc/html/boost_asio/reference/buffer.html#boost_asio.reference.buffer.accessing_buffer_contents +				// Although apr_file_write() accepts const void*, we +				// manipulate const char* so we can increment the pointer. +				const char* remainptr = boost::asio::buffer_cast<const char*>(*bufi); +				std::size_t remainlen = boost::asio::buffer_size(*bufi); +				while (remainlen) +				{ +					// Tackle the current buffer in discrete chunks. On +					// Windows, we've observed strange failures when trying to +					// write big lengths (~1 MB) in a single operation. Even a +					// 32K chunk seems too large. At some point along the way +					// apr_file_write() returns 11 (Resource temporarily +					// unavailable, i.e. EAGAIN) and says it wrote 0 bytes -- +					// even though it did write the chunk! Our next write +					// attempt retries with the same chunk, resulting in the +					// chunk being duplicated at the child end. Using smaller +					// chunks is empirically more reliable. +					std::size_t towrite((std::min)(remainlen, std::size_t(4*1024))); +					apr_size_t written(towrite); +					apr_status_t err = apr_file_write(mPipe, remainptr, &written); +					// EAGAIN is exactly what we want from a nonblocking pipe. +					// Rather than waiting for data, it should return immediately. +					if (! (err == APR_SUCCESS || APR_STATUS_IS_EAGAIN(err))) +					{ +						LL_WARNS("LLProcess") << "apr_file_write(" << towrite << ") on " << mDesc +											  << " got " << err << ":" << LL_ENDL; +						ll_apr_warn_status(err); +					} + +					// 'written' is modified to reflect the number of bytes actually +					// written. Make sure we consume those later. (Don't consume them +					// now, that would invalidate the buffer iterator sequence!) +					consumed += written; +					// don't forget to advance to next chunk of current buffer +					remainptr += written; +					remainlen -= written; + +					char msgbuf[512]; +					LL_DEBUGS("LLProcess") << "wrote " << written << " of " << towrite +										   << " bytes to " << mDesc +										   << " (original " << total << ")," +										   << " code " << err << ": " +										   << apr_strerror(err, msgbuf, sizeof(msgbuf)) +										   << LL_ENDL; + +					// The parent end of this pipe is nonblocking. If we weren't able +					// to write everything we wanted, don't keep banging on it -- that +					// won't change until the child reads some. Wait for next tick(). +					if (written < towrite) +					{ +						keepwriting = false; // break outer loop over buffers too +						break; +					} +				} // next chunk of current buffer +			}     // next buffer +			// In all, we managed to write 'consumed' bytes. Remove them from the +			// streambuf so we don't keep trying to send them. This could be +			// anywhere from 0 up to mStreambuf.size(); anything we haven't yet +			// sent, we'll try again later. +			mStreambuf.consume(consumed); +		} + +		return false; +	} + +private: +	std::string mDesc; +	apr_file_t* mPipe; +	LLTempBoundListener mConnection; +	boost::asio::streambuf mStreambuf; +	std::ostream mStream; +}; + +class ReadPipeImpl: public LLProcess::ReadPipe +{ +	LOG_CLASS(ReadPipeImpl); +public: +	ReadPipeImpl(const std::string& desc, apr_file_t* pipe, LLProcess::FILESLOT index): +		mDesc(desc), +		mPipe(pipe), +		mIndex(index), +		// Essential to initialize our std::istream with our special streambuf! +		mStream(&mStreambuf), +		mPump("ReadPipe", true),    // tweak name as needed to avoid collisions +		mLimit(0), +		mEOF(false) +	{ +		mConnection = LLEventPumps::instance().obtain("mainloop") +			.listen(LLEventPump::inventName("ReadPipe"), +					boost::bind(&ReadPipeImpl::tick, this, _1)); +	} + +	// Much of the implementation is simply connecting the abstract virtual +	// methods with implementation data concealed from the base class. +	virtual std::istream& get_istream() { return mStream; } +	virtual std::string getline() { return LLProcess::getline(mStream); } +	virtual LLEventPump& getPump() { return mPump; } +	virtual void setLimit(size_type limit) { mLimit = limit; } +	virtual size_type getLimit() const { return mLimit; } +	virtual size_type size() const { return mStreambuf.size(); } + +	virtual std::string read(size_type len) +	{ +		// Read specified number of bytes into a buffer. Make a buffer big +		// enough. +		size_type readlen((std::min)(size(), len)); +		std::vector<char> buffer(readlen); +		mStream.read(&buffer[0], readlen); +		// Since we've already clamped 'readlen', we can think of no reason +		// why mStream.read() should read fewer than 'readlen' bytes. +		// Nonetheless, use the actual retrieved length. +		return std::string(&buffer[0], mStream.gcount()); +	} + +	virtual std::string peek(size_type offset=0, size_type len=npos) const +	{ +		// Constrain caller's offset and len to overlap actual buffer content. +		std::size_t real_offset = (std::min)(mStreambuf.size(), std::size_t(offset)); +		size_type	want_end	= (len == npos)? npos : (real_offset + len); +		std::size_t real_end	= (std::min)(mStreambuf.size(), std::size_t(want_end)); +		boost::asio::streambuf::const_buffers_type cbufs = mStreambuf.data(); +		return std::string(boost::asio::buffers_begin(cbufs) + real_offset, +						   boost::asio::buffers_begin(cbufs) + real_end); +	} + +	virtual size_type find(const std::string& seek, size_type offset=0) const +	{ +		// If we're passing a string of length 1, use find(char), which can +		// use an O(n) std::find() rather than the O(n^2) std::search(). +		if (seek.length() == 1) +		{ +			return find(seek[0], offset); +		} + +		// If offset is beyond the whole buffer, can't even construct a valid +		// iterator range; can't possibly find the string we seek. +		if (offset > mStreambuf.size()) +		{ +			return npos; +		} + +		boost::asio::streambuf::const_buffers_type cbufs = mStreambuf.data(); +		boost::asio::buffers_iterator<boost::asio::streambuf::const_buffers_type> +			begin(boost::asio::buffers_begin(cbufs)), +			end	 (boost::asio::buffers_end(cbufs)), +			found(std::search(begin + offset, end, seek.begin(), seek.end())); +		return (found == end)? npos : (found - begin); +	} + +	virtual size_type find(char seek, size_type offset=0) const +	{ +		// If offset is beyond the whole buffer, can't even construct a valid +		// iterator range; can't possibly find the char we seek. +		if (offset > mStreambuf.size()) +		{ +			return npos; +		} + +		boost::asio::streambuf::const_buffers_type cbufs = mStreambuf.data(); +		boost::asio::buffers_iterator<boost::asio::streambuf::const_buffers_type> +			begin(boost::asio::buffers_begin(cbufs)), +			end	 (boost::asio::buffers_end(cbufs)), +			found(std::find(begin + offset, end, seek)); +		return (found == end)? npos : (found - begin); +	} + +	bool tick(const LLSD&) +	{ +		// Once we've hit EOF, skip all the rest of this. +		if (mEOF) +			return false; + +		typedef boost::asio::streambuf::mutable_buffers_type mutable_buffer_sequence; +		// Try, every time, to read into our streambuf. In fact, we have no +		// idea how much data the child might be trying to send: keep trying +		// until we're convinced we've temporarily exhausted the pipe. +		enum PipeState { RETRY, EXHAUSTED, CLOSED }; +		PipeState state = RETRY; +		std::size_t committed(0); +		do +		{ +			// attempt to read an arbitrary size +			mutable_buffer_sequence bufs = mStreambuf.prepare(4096); +			// In general, the mutable_buffer_sequence returned by prepare() might +			// contain a number of different physical buffers; iterate over those. +			std::size_t tocommit(0); +			for (mutable_buffer_sequence::const_iterator bufi(bufs.begin()), bufend(bufs.end()); +				 bufi != bufend; ++bufi) +			{ +				// http://www.boost.org/doc/libs/1_49_0_beta1/doc/html/boost_asio/reference/buffer.html#boost_asio.reference.buffer.accessing_buffer_contents +				std::size_t toread(boost::asio::buffer_size(*bufi)); +				apr_size_t gotten(toread); +				apr_status_t err = apr_file_read(mPipe, +												 boost::asio::buffer_cast<void*>(*bufi), +												 &gotten); +				// EAGAIN is exactly what we want from a nonblocking pipe. +				// Rather than waiting for data, it should return immediately. +				if (! (err == APR_SUCCESS || APR_STATUS_IS_EAGAIN(err))) +				{ +					// Handle EOF specially: it's part of normal-case processing. +					if (err == APR_EOF) +					{ +						LL_DEBUGS("LLProcess") << "EOF on " << mDesc << LL_ENDL; +					} +					else +					{ +						LL_WARNS("LLProcess") << "apr_file_read(" << toread << ") on " << mDesc +											  << " got " << err << ":" << LL_ENDL; +						ll_apr_warn_status(err); +					} +					// Either way, though, we won't need any more tick() calls. +					mConnection.disconnect(); +					// Ignore any subsequent calls we might get anyway. +					mEOF = true; +					state = CLOSED; // also break outer retry loop +					break; +				} + +				// 'gotten' was modified to reflect the number of bytes actually +				// received. Make sure we commit those later. (Don't commit them +				// now, that would invalidate the buffer iterator sequence!) +				tocommit += gotten; +				LL_DEBUGS("LLProcess") << "filled " << gotten << " of " << toread +									   << " bytes from " << mDesc << LL_ENDL; + +				// The parent end of this pipe is nonblocking. If we weren't even +				// able to fill this buffer, don't loop to try to fill the next -- +				// that won't change until the child writes more. Wait for next +				// tick(). +				if (gotten < toread) +				{ +					// break outer retry loop too +					state = EXHAUSTED; +					break; +				} +			} + +			// Don't forget to "commit" the data! +			mStreambuf.commit(tocommit); +			committed += tocommit; + +			// state is changed from RETRY when we can't fill any one buffer +			// of the mutable_buffer_sequence established by the current +			// prepare() call -- whether due to error or not enough bytes. +			// That is, if state is still RETRY, we've filled every physical +			// buffer in the mutable_buffer_sequence. In that case, for all we +			// know, the child might have still more data pending -- go for it! +		} while (state == RETRY); + +		// Once we recognize that the pipe is closed, make one more call to +		// listener. The listener might be waiting for a particular substring +		// to arrive, or a particular length of data or something. The event +		// with "eof" == true announces that nothing further will arrive, so +		// use it or lose it. +		if (committed || state == CLOSED) +		{ +			// If we actually received new data, publish it on our LLEventPump +			// as advertised. Constrain it by mLimit. But show listener the +			// actual accumulated buffer size, regardless of mLimit. +			size_type datasize((std::min)(mLimit, size_type(mStreambuf.size()))); +			mPump.post(LLSDMap +					   ("data", peek(0, datasize)) +					   ("len", LLSD::Integer(mStreambuf.size())) +					   ("slot", LLSD::Integer(mIndex)) +					   ("name", whichfile(mIndex)) +					   ("desc", mDesc) +					   ("eof", state == CLOSED)); +		} + +		return false; +	} + +private: +	std::string mDesc; +	apr_file_t* mPipe; +	LLProcess::FILESLOT mIndex; +	LLTempBoundListener mConnection; +	boost::asio::streambuf mStreambuf; +	std::istream mStream; +	LLEventStream mPump; +	size_type mLimit; +	bool mEOF; +}; + +/***************************************************************************** +*   LLProcess itself +*****************************************************************************/ +/// Need an exception to avoid constructing an invalid LLProcess object, but +/// internal use only +struct LLProcessError: public std::runtime_error +{ +	LLProcessError(const std::string& msg): std::runtime_error(msg) {} +}; + +LLProcessPtr LLProcess::create(const LLSDOrParams& params) +{ +	try +	{ +		return LLProcessPtr(new LLProcess(params)); +	} +	catch (const LLProcessError& e) +	{ +		LL_WARNS("LLProcess") << e.what() << LL_ENDL; + +		// If caller is requesting an event on process termination, send one +		// indicating bad launch. This may prevent someone waiting forever for +		// a termination post that can't arrive because the child never +		// started. +		if (params.postend.isProvided()) +		{ +			LLEventPumps::instance().obtain(params.postend) +				.post(LLSDMap +					  // no "id" +					  ("desc", getDesc(params)) +					  ("state", LLProcess::UNSTARTED) +					  // no "data" +					  ("string", e.what()) +					 ); +		} + +		return LLProcessPtr(); +	} +} + +/// Call an apr function returning apr_status_t. On failure, log warning and +/// throw LLProcessError mentioning the function call that produced that +/// result. +#define chkapr(func)                            \ +	if (ll_apr_warn_status(func))				\ +		throw LLProcessError(#func " failed") + +LLProcess::LLProcess(const LLSDOrParams& params): +	mAutokill(params.autokill), +	mPipes(NSLOTS) +{ +	// Hmm, when you construct a ptr_vector with a size, it merely reserves +	// space, it doesn't actually make it that big. Explicitly make it bigger. +	// Because of ptr_vector's odd semantics, have to push_back(0) the right +	// number of times! resize() wants to default-construct new BasePipe +	// instances, which fails because it's pure virtual. But because of the +	// constructor call, these push_back() calls should require no new +	// allocation. +	for (size_t i = 0; i < mPipes.capacity(); ++i) +		mPipes.push_back(0); + +	if (! params.validateBlock(true)) +	{ +		throw LLProcessError(STRINGIZE("not launched: failed parameter validation\n" +									   << LLSDNotationStreamer(params))); +	} + +	mPostend = params.postend; + +	apr_procattr_t *procattr = NULL; +	chkapr(apr_procattr_create(&procattr, gAPRPoolp)); + +	// For which of stdin, stdout, stderr should we create a pipe to the +	// child? In the viewer, there are only a couple viable +	// apr_procattr_io_set() alternatives: inherit the viewer's own stdxxx +	// handle (APR_NO_PIPE, e.g. for stdout, stderr), or create a pipe that's +	// blocking on the child end but nonblocking at the viewer end +	// (APR_CHILD_BLOCK). +	// Other major options could include explicitly creating a single APR pipe +	// and passing it as both stdout and stderr (apr_procattr_child_out_set(), +	// apr_procattr_child_err_set()), or accepting a filename, opening it and +	// passing that apr_file_t (simple <, >, 2> redirect emulation). +	std::vector<apr_int32_t> select; +	BOOST_FOREACH(const FileParam& fparam, params.files) +	{ +		// Every iteration, we're going to append an item to 'select'. At the +		// top of the loop, its size() is, in effect, an index. Use that to +		// pick a string description for messages. +		std::string which(whichfile(FILESLOT(select.size()))); +		if (fparam.type().empty())  // inherit our file descriptor +		{ +			select.push_back(APR_NO_PIPE); +		} +		else if (fparam.type() == "pipe") // anonymous pipe +		{ +			if (! fparam.name().empty()) +			{ +				LL_WARNS("LLProcess") << "For " << params.executable() +									  << ": internal names for reusing pipes ('" +									  << fparam.name() << "' for " << which +									  << ") are not yet supported -- creating distinct pipe" +									  << LL_ENDL; +			} +			// The viewer can't block for anything: the parent end MUST be +			// nonblocking. As the APR documentation itself points out, it +			// makes very little sense to set nonblocking I/O for the child +			// end of a pipe: only a specially-written child could deal with +			// that. +			select.push_back(APR_CHILD_BLOCK); +		} +		else +		{ +			throw LLProcessError(STRINGIZE("For " << params.executable() +										   << ": unsupported FileParam for " << which +										   << ": type='" << fparam.type() +										   << "', name='" << fparam.name() << "'")); +		} +	} +	// By default, pass APR_NO_PIPE for unspecified slots. +	while (select.size() < NSLOTS) +	{ +		select.push_back(APR_NO_PIPE); +	} +	chkapr(apr_procattr_io_set(procattr, select[STDIN], select[STDOUT], select[STDERR])); + +	// Thumbs down on implicitly invoking the shell to invoke the child. From +	// our point of view, the other major alternative to APR_PROGRAM_PATH +	// would be APR_PROGRAM_ENV: still copy environment, but require full +	// executable pathname. I don't see a downside to searching the PATH, +	// though: if our caller wants (e.g.) a specific Python interpreter, s/he +	// can still pass the full pathname. +	chkapr(apr_procattr_cmdtype_set(procattr, APR_PROGRAM_PATH)); +	// YES, do extra work if necessary to report child exec() failures back to +	// parent process. +	chkapr(apr_procattr_error_check_set(procattr, 1)); +	// Do not start a non-autokill child in detached state. On Posix +	// platforms, this setting attempts to daemonize the new child, closing +	// std handles and the like, and that's a bit more detachment than we +	// want. autokill=false just means not to implicitly kill the child when +	// the parent terminates! +//	chkapr(apr_procattr_detach_set(procattr, params.autokill? 0 : 1)); + +	if (params.autokill) +	{ +#if defined(APR_HAS_PROCATTR_AUTOKILL_SET) +		apr_status_t ok = apr_procattr_autokill_set(procattr, 1); +# if LL_WINDOWS +		// As of 2012-02-02, we only expect this to be implemented on Windows. +		// Avoid spamming the log with warnings we fully expect. +		ll_apr_warn_status(ok); +#else   // ! LL_WINDOWS +		(void)ok;                   // suppress 'unused' warning +# endif // ! LL_WINDOWS +#else +		LL_WARNS("LLProcess") << "This version of APR lacks Linden apr_procattr_autokill_set() extension" << LL_ENDL; +#endif +	} + +	// In preparation for calling apr_proc_create(), we collect a number of +	// const char* pointers obtained from std::string::c_str(). Turns out +	// LLInitParam::Block's helpers Optional, Mandatory, Multiple et al. +	// guarantee that converting to the wrapped type (std::string in our +	// case), e.g. by calling operator(), returns a reference to *the same +	// instance* of the wrapped type that's stored in our Block subclass. +	// That's important! We know 'params' persists throughout this method +	// call; but without that guarantee, we'd have to assume that converting +	// one of its members to std::string might return a different (temp) +	// instance. Capturing the c_str() from a temporary std::string is Bad Bad +	// Bad. But armed with this knowledge, when you see params.cwd().c_str(), +	// grit your teeth and smile and carry on. + +	if (params.cwd.isProvided()) +	{ +		chkapr(apr_procattr_dir_set(procattr, params.cwd().c_str())); +	} + +	// create an argv vector for the child process +	std::vector<const char*> argv; + +	// Add the executable path. See above remarks about c_str(). +	argv.push_back(params.executable().c_str()); + +	// Add arguments. See above remarks about c_str(). +	BOOST_FOREACH(const std::string& arg, params.args) +	{ +		argv.push_back(arg.c_str()); +	} + +	// terminate with a null pointer +	argv.push_back(NULL); + +	// Launch! The NULL would be the environment block, if we were passing +	// one. Hand-expand chkapr() macro so we can fill in the actual command +	// string instead of the variable names. +	if (ll_apr_warn_status(apr_proc_create(&mProcess, argv[0], &argv[0], NULL, procattr, +										   gAPRPoolp))) +	{ +		throw LLProcessError(STRINGIZE(params << " failed")); +	} + +	// arrange to call status_callback() +	apr_proc_other_child_register(&mProcess, &LLProcess::status_callback, this, mProcess.in, +								  gAPRPoolp); +	// and make sure we poll it once per "mainloop" tick +	sProcessListener.addPoll(*this); +	mStatus.mState = RUNNING; + +	mDesc = STRINGIZE(getDesc(params) << " (" << mProcess.pid << ')'); +	LL_INFOS("LLProcess") << mDesc << ": launched " << params << LL_ENDL; + +	// Unless caller explicitly turned off autokill (child should persist), +	// take steps to terminate the child. This is all suspenders-and-belt: in +	// theory our destructor should kill an autokill child, but in practice +	// that doesn't always work (e.g. VWR-21538). +	if (params.autokill) +	{ +/*==========================================================================*| +		// NO: There may be an APR bug, not sure -- but at least on Mac, when +		// gAPRPoolp is destroyed, OUR process receives SIGTERM! Apparently +		// either our own PID is getting into the list of processes to kill() +		// (unlikely), or somehow one of those PIDs is getting zeroed first, +		// so that kill() sends SIGTERM to the whole process group -- this +		// process included. I'd have to build and link with a debug version +		// of APR to know for sure. It's too bad: this mechanism would be just +		// right for dealing with static autokill LLProcessPtr variables, +		// which aren't destroyed until after APR is no longer available. + +		// Tie the lifespan of this child process to the lifespan of our APR +		// pool: on destruction of the pool, forcibly kill the process. Tell +		// APR to try SIGTERM and wait 3 seconds. If that didn't work, use +		// SIGKILL. +		apr_pool_note_subprocess(gAPRPoolp, &mProcess, APR_KILL_AFTER_TIMEOUT); +|*==========================================================================*/ + +		// On Windows, associate the new child process with our Job Object. +		autokill(); +	} + +	// Instantiate the proper pipe I/O machinery +	// want to be able to point to apr_proc_t::in, out, err by index +	typedef apr_file_t* apr_proc_t::*apr_proc_file_ptr; +	static apr_proc_file_ptr members[] = +		{ &apr_proc_t::in, &apr_proc_t::out, &apr_proc_t::err }; +	for (size_t i = 0; i < NSLOTS; ++i) +	{ +		if (select[i] != APR_CHILD_BLOCK) +			continue; +		std::string desc(STRINGIZE(mDesc << ' ' << whichfile(FILESLOT(i)))); +		apr_file_t* pipe(mProcess.*(members[i])); +		if (i == STDIN) +		{ +			mPipes.replace(i, new WritePipeImpl(desc, pipe)); +		} +		else +		{ +			mPipes.replace(i, new ReadPipeImpl(desc, pipe, FILESLOT(i))); +		} +		LL_DEBUGS("LLProcess") << "Instantiating " << typeid(mPipes[i]).name() +							   << "('" << desc << "')" << LL_ENDL; +	} +} + +// Helper to obtain a description string, given a Params block +static std::string getDesc(const LLProcess::Params& params) +{ +	// If caller specified a description string, by all means use it. +	if (params.desc.isProvided()) +		return params.desc; + +	// Caller didn't say. Use the executable name -- but use just the filename +	// part. On Mac, for instance, full pathnames get cumbersome. +	return LLProcess::basename(params.executable); +} + +//static +std::string LLProcess::basename(const std::string& path) +{ +	// If there are Linden utility functions to manipulate pathnames, I +	// haven't found them -- and for this usage, Boost.Filesystem seems kind +	// of heavyweight. +	std::string::size_type delim = path.find_last_of("\\/"); +	// If path contains no pathname delimiters, return the whole thing. +	if (delim == std::string::npos) +		return path; + +	// Return just the part beyond the last delimiter. +	return path.substr(delim + 1); +} + +LLProcess::~LLProcess() +{ +	// In the Linden viewer, there's at least one static LLProcessPtr. Its +	// destructor will be called *after* ll_cleanup_apr(). In such a case, +	// unregistering is pointless (and fatal!) -- and kill(), which also +	// relies on APR, is impossible. +	if (! gAPRPoolp) +		return; + +	// Only in state RUNNING are we registered for callback. In UNSTARTED we +	// haven't yet registered. And since receiving the callback is the only +	// way we detect child termination, we only change from state RUNNING at +	// the same time we unregister. +	if (mStatus.mState == RUNNING) +	{ +		// We're still registered for a callback: unregister. Do it before +		// we even issue the kill(): even if kill() somehow prompted an +		// instantaneous callback (unlikely), this object is going away! Any +		// information updated in this object by such a callback is no longer +		// available to any consumer anyway. +		apr_proc_other_child_unregister(this); +		// One less LLProcess to poll for +		sProcessListener.dropPoll(*this); +	} + +	if (mAutokill) +	{ +		kill("destructor"); +	} +} + +bool LLProcess::kill(const std::string& who) +{ +	if (isRunning()) +	{ +		LL_INFOS("LLProcess") << who << " killing " << mDesc << LL_ENDL; + +#if LL_WINDOWS +		int sig = -1; +#else  // Posix +		int sig = SIGTERM; +#endif + +		ll_apr_warn_status(apr_proc_kill(&mProcess, sig)); +	} + +	return ! isRunning(); +} + +bool LLProcess::isRunning() const +{ +	return getStatus().mState == RUNNING; +} + +LLProcess::Status LLProcess::getStatus() const +{ +	return mStatus; +} + +std::string LLProcess::getStatusString() const +{ +	return getStatusString(getStatus()); +} + +std::string LLProcess::getStatusString(const Status& status) const +{ +	return getStatusString(mDesc, status); +} + +//static +std::string LLProcess::getStatusString(const std::string& desc, const Status& status) +{ +	if (status.mState == UNSTARTED) +		return desc + " was never launched"; + +	if (status.mState == RUNNING) +		return desc + " running"; + +	if (status.mState == EXITED) +		return STRINGIZE(desc << " exited with code " << status.mData); + +	if (status.mState == KILLED) +#if LL_WINDOWS +		return STRINGIZE(desc << " killed with exception " << std::hex << status.mData); +#else +		return STRINGIZE(desc << " killed by signal " << status.mData +						 << " (" << apr_signal_description_get(status.mData) << ")"); +#endif + +	return STRINGIZE(desc << " in unknown state " << status.mState << " (" << status.mData << ")"); +} + +// Classic-C-style APR callback +void LLProcess::status_callback(int reason, void* data, int status) +{ +	// Our only role is to bounce this static method call back into object +	// space. +	static_cast<LLProcess*>(data)->handle_status(reason, status); +} + +#define tabent(symbol) { symbol, #symbol } +static struct ReasonCode +{ +	int code; +	const char* name; +} reasons[] = +{ +	tabent(APR_OC_REASON_DEATH), +	tabent(APR_OC_REASON_UNWRITABLE), +	tabent(APR_OC_REASON_RESTART), +	tabent(APR_OC_REASON_UNREGISTER), +	tabent(APR_OC_REASON_LOST), +	tabent(APR_OC_REASON_RUNNING) +}; +#undef tabent + +// Object-oriented callback +void LLProcess::handle_status(int reason, int status) +{ +	{ +		// This odd appearance of LL_DEBUGS is just to bracket a lookup that will +		// only be performed if in fact we're going to produce the log message. +		LL_DEBUGS("LLProcess") << empty; +		std::string reason_str; +		BOOST_FOREACH(const ReasonCode& rcp, reasons) +		{ +			if (reason == rcp.code) +			{ +				reason_str = rcp.name; +				break; +			} +		} +		if (reason_str.empty()) +		{ +			reason_str = STRINGIZE("unknown reason " << reason); +		} +		LL_CONT << mDesc << ": handle_status(" << reason_str << ", " << status << ")" << LL_ENDL; +	} + +	if (! (reason == APR_OC_REASON_DEATH || reason == APR_OC_REASON_LOST)) +	{ +		// We're only interested in the call when the child terminates. +		return; +	} + +	// Somewhat oddly, APR requires that you explicitly unregister even when +	// it already knows the child has terminated. We must pass the same 'data' +	// pointer as for the register() call, which was our 'this'. +	apr_proc_other_child_unregister(this); +	// don't keep polling for a terminated process +	sProcessListener.dropPoll(*this); +	// We overload mStatus.mState to indicate whether the child is registered +	// for APR callback: only RUNNING means registered. Track that we've +	// unregistered. We know the child has terminated; might be EXITED or +	// KILLED; refine below. +	mStatus.mState = EXITED; + +	// Make last-gasp calls for each of the ReadPipes we have on hand. Since +	// they're listening on "mainloop", we can be sure they'll eventually +	// collect all pending data from the child. But we want to be able to +	// guarantee to our consumer that by the time we post on the "postend" +	// LLEventPump, our ReadPipes are already buffering all the data there +	// will ever be from the child. That lets the "postend" listener decide +	// what to do with that final data. +	for (size_t i = 0; i < mPipes.size(); ++i) +	{ +		std::string error; +		ReadPipeImpl* ppipe = getPipePtr<ReadPipeImpl>(error, FILESLOT(i)); +		if (ppipe) +		{ +			static LLSD trivial; +			ppipe->tick(trivial); +		} +	} + +//	wi->rv = apr_proc_wait(wi->child, &wi->rc, &wi->why, APR_NOWAIT); +	// It's just wrong to call apr_proc_wait() here. The only way APR knows to +	// call us with APR_OC_REASON_DEATH is that it's already reaped this child +	// process, so calling wait() will only produce "huh?" from the OS. We +	// must rely on the status param passed in, which unfortunately comes +	// straight from the OS wait() call, which means we have to decode it by +	// hand. +	mStatus = interpret_status(status); +	LL_INFOS("LLProcess") << getStatusString() << LL_ENDL; + +	// If caller requested notification on child termination, send it. +	if (! mPostend.empty()) +	{ +		LLEventPumps::instance().obtain(mPostend) +			.post(LLSDMap +				  ("id",     getProcessID()) +				  ("desc",   mDesc) +				  ("state",  mStatus.mState) +				  ("data",   mStatus.mData) +				  ("string", getStatusString()) +				 ); +	} +} + +LLProcess::id LLProcess::getProcessID() const +{ +	return mProcess.pid; +} + +LLProcess::handle LLProcess::getProcessHandle() const +{ +#if LL_WINDOWS +	return mProcess.hproc; +#else +	return mProcess.pid; +#endif +} + +std::string LLProcess::getPipeName(FILESLOT) const +{ +	// LLProcess::FileParam::type "npipe" is not yet implemented +	return ""; +} + +template<class PIPETYPE> +PIPETYPE* LLProcess::getPipePtr(std::string& error, FILESLOT slot) +{ +	if (slot >= NSLOTS) +	{ +		error = STRINGIZE(mDesc << " has no slot " << slot); +		return NULL; +	} +	if (mPipes.is_null(slot)) +	{ +		error = STRINGIZE(mDesc << ' ' << whichfile(slot) << " not a monitored pipe"); +		return NULL; +	} +	// Make sure we dynamic_cast in pointer domain so we can test, rather than +	// accepting runtime's exception. +	PIPETYPE* ppipe = dynamic_cast<PIPETYPE*>(&mPipes[slot]); +	if (! ppipe) +	{ +		error = STRINGIZE(mDesc << ' ' << whichfile(slot) << " not a " << typeid(PIPETYPE).name()); +		return NULL; +	} + +	error.clear(); +	return ppipe; +} + +template <class PIPETYPE> +PIPETYPE& LLProcess::getPipe(FILESLOT slot) +{ +	std::string error; +	PIPETYPE* wp = getPipePtr<PIPETYPE>(error, slot); +	if (! wp) +	{ +		throw NoPipe(error); +	} +	return *wp; +} + +template <class PIPETYPE> +boost::optional<PIPETYPE&> LLProcess::getOptPipe(FILESLOT slot) +{ +	std::string error; +	PIPETYPE* wp = getPipePtr<PIPETYPE>(error, slot); +	if (! wp) +	{ +		LL_DEBUGS("LLProcess") << error << LL_ENDL; +		return boost::optional<PIPETYPE&>(); +	} +	return *wp; +} + +LLProcess::WritePipe& LLProcess::getWritePipe(FILESLOT slot) +{ +	return getPipe<WritePipe>(slot); +} + +boost::optional<LLProcess::WritePipe&> LLProcess::getOptWritePipe(FILESLOT slot) +{ +	return getOptPipe<WritePipe>(slot); +} + +LLProcess::ReadPipe& LLProcess::getReadPipe(FILESLOT slot) +{ +	return getPipe<ReadPipe>(slot); +} + +boost::optional<LLProcess::ReadPipe&> LLProcess::getOptReadPipe(FILESLOT slot) +{ +	return getOptPipe<ReadPipe>(slot); +} + +//static +std::string LLProcess::getline(std::istream& in) +{ +	std::string line; +	std::getline(in, line); +	// Blur the distinction between "\r\n" and plain "\n". std::getline() will +	// have eaten the "\n", but we could still end up with a trailing "\r". +	std::string::size_type lastpos = line.find_last_not_of("\r"); +	if (lastpos != std::string::npos) +	{ +		// Found at least one character that's not a trailing '\r'. SKIP OVER +		// IT and erase the rest of the line. +		line.erase(lastpos+1); +	} +	return line; +} + +std::ostream& operator<<(std::ostream& out, const LLProcess::Params& params) +{ +	if (params.cwd.isProvided()) +	{ +		out << "cd " << LLStringUtil::quote(params.cwd) << ": "; +	} +	out << LLStringUtil::quote(params.executable); +	BOOST_FOREACH(const std::string& arg, params.args) +	{ +		out << ' ' << LLStringUtil::quote(arg); +	} +	return out; +} + +/***************************************************************************** +*   Windows specific +*****************************************************************************/ +#if LL_WINDOWS + +static std::string WindowsErrorString(const std::string& operation); + +void LLProcess::autokill() +{ +	// hopefully now handled by apr_procattr_autokill_set() +} + +LLProcess::handle LLProcess::isRunning(handle h, const std::string& desc) +{ +	// This direct Windows implementation is because we have no access to the +	// apr_proc_t struct: we expect it's been destroyed. +	if (! h) +		return 0; + +	DWORD waitresult = WaitForSingleObject(h, 0); +	if(waitresult == WAIT_OBJECT_0) +	{ +		// the process has completed. +		if (! desc.empty()) +		{ +			DWORD status = 0; +			if (! GetExitCodeProcess(h, &status)) +			{ +				LL_WARNS("LLProcess") << desc << " terminated, but " +									  << WindowsErrorString("GetExitCodeProcess()") << LL_ENDL; +			} +			{ +				LL_INFOS("LLProcess") << getStatusString(desc, interpret_status(status)) +									  << LL_ENDL; +			} +		} +		CloseHandle(h); +		return 0; +	} + +	return h; +} + +static LLProcess::Status interpret_status(int status) +{ +	LLProcess::Status result; + +	// This bit of code is cribbed from apr/threadproc/win32/proc.c, a +	// function (unfortunately static) called why_from_exit_code(): +	/* See WinNT.h STATUS_ACCESS_VIOLATION and family for how +	 * this class of failures was determined +	 */ +	if ((status & 0xFFFF0000) == 0xC0000000) +	{ +		result.mState = LLProcess::KILLED; +	} +	else +	{ +		result.mState = LLProcess::EXITED; +	} +	result.mData = status; + +	return result; +} + +/// GetLastError()/FormatMessage() boilerplate +static std::string WindowsErrorString(const std::string& operation) +{ +	int result = GetLastError(); + +	LPTSTR error_str = 0; +	if (FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, +					   NULL, +					   result, +					   0, +					   (LPTSTR)&error_str, +					   0, +					   NULL) +		!= 0)  +	{ +		// convert from wide-char string to multi-byte string +		char message[256]; +		wcstombs(message, error_str, sizeof(message)); +		message[sizeof(message)-1] = 0; +		LocalFree(error_str); +		// convert to std::string to trim trailing whitespace +		std::string mbsstr(message); +		mbsstr.erase(mbsstr.find_last_not_of(" \t\r\n")); +		return STRINGIZE(operation << " failed (" << result << "): " << mbsstr); +	} +	return STRINGIZE(operation << " failed (" << result +					 << "), but FormatMessage() did not explain"); +} + +/***************************************************************************** +*   Posix specific +*****************************************************************************/ +#else // Mac and linux + +#include <signal.h> +#include <fcntl.h> +#include <errno.h> +#include <sys/wait.h> + +void LLProcess::autokill() +{ +	// What we ought to do here is to: +	// 1. create a unique process group and run all autokill children in that +	//    group (see https://jira.secondlife.com/browse/SWAT-563); +	// 2. figure out a way to intercept control when the viewer exits -- +	//    gracefully or not;  +	// 3. when the viewer exits, kill off the aforementioned process group. + +	// It's point 2 that's troublesome. Although I've seen some signal- +	// handling logic in the Posix viewer code, I haven't yet found any bit of +	// code that's run no matter how the viewer exits (a try/finally for the +	// whole process, as it were). +} + +// Attempt to reap a process ID -- returns true if the process has exited and been reaped, false otherwise. +static bool reap_pid(pid_t pid, LLProcess::Status* pstatus=NULL) +{ +	LLProcess::Status dummy; +	if (! pstatus) +	{ +		// If caller doesn't want to see Status, give us a target anyway so we +		// don't have to have a bunch of conditionals. +		pstatus = &dummy; +	} + +	int status = 0; +	pid_t wait_result = ::waitpid(pid, &status, WNOHANG); +	if (wait_result == pid) +	{ +		*pstatus = interpret_status(status); +		return true; +	} +	if (wait_result == 0) +	{ +		pstatus->mState = LLProcess::RUNNING; +		pstatus->mData	= 0; +		return false; +	} + +	// Clear caller's Status block; caller must interpret UNSTARTED to mean +	// "if this PID was ever valid, it no longer is." +	*pstatus = LLProcess::Status(); + +	// We've dealt with the success cases: we were able to reap the child +	// (wait_result == pid) or it's still running (wait_result == 0). It may +	// be that the child terminated but didn't hang around long enough for us +	// to reap. In that case we still have no Status to report, but we can at +	// least state that it's not running. +	if (wait_result == -1 && errno == ECHILD) +	{ +		// No such process -- this may mean we're ignoring SIGCHILD. +		return true; +	} + +	// Uh, should never happen?! +	LL_WARNS("LLProcess") << "LLProcess::reap_pid(): waitpid(" << pid << ") returned " +						  << wait_result << "; not meaningful?" << LL_ENDL; +	// If caller is looping until this pid terminates, and if we can't find +	// out, better to break the loop than to claim it's still running. +	return true; +} + +LLProcess::id LLProcess::isRunning(id pid, const std::string& desc) +{ +	// This direct Posix implementation is because we have no access to the +	// apr_proc_t struct: we expect it's been destroyed. +	if (! pid) +		return 0; + +	// Check whether the process has exited, and reap it if it has. +	LLProcess::Status status; +	if(reap_pid(pid, &status)) +	{ +		// the process has exited. +		if (! desc.empty()) +		{ +			std::string statstr(desc + " apparently terminated: no status available"); +			// We don't just pass UNSTARTED to getStatusString() because, in +			// the context of reap_pid(), that state has special meaning. +			if (status.mState != UNSTARTED) +			{ +				statstr = getStatusString(desc, status); +			} +			LL_INFOS("LLProcess") << statstr << LL_ENDL; +		} +		return 0; +	} + +	return pid; +} + +static LLProcess::Status interpret_status(int status) +{ +	LLProcess::Status result; + +	if (WIFEXITED(status)) +	{ +		result.mState = LLProcess::EXITED; +		result.mData  = WEXITSTATUS(status); +	} +	else if (WIFSIGNALED(status)) +	{ +		result.mState = LLProcess::KILLED; +		result.mData  = WTERMSIG(status); +	} +	else                            // uh, shouldn't happen? +	{ +		result.mState = LLProcess::EXITED; +		result.mData  = status;     // someone else will have to decode +	} + +	return result; +} + +#endif  // Posix diff --git a/indra/llcommon/llprocess.h b/indra/llcommon/llprocess.h new file mode 100644 index 0000000000..51010966f9 --- /dev/null +++ b/indra/llcommon/llprocess.h @@ -0,0 +1,546 @@ +/**  + * @file llprocess.h + * @brief Utility class for launching, terminating, and tracking child processes. + * + * $LicenseInfo:firstyear=2008&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, Linden Research, Inc. + *  + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + *  + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU + * Lesser General Public License for more details. + *  + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA + *  + * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA + * $/LicenseInfo$ + */ +  +#ifndef LL_LLPROCESS_H +#define LL_LLPROCESS_H + +#include "llinitparam.h" +#include "llsdparam.h" +#include "apr_thread_proc.h" +#include <boost/shared_ptr.hpp> +#include <boost/ptr_container/ptr_vector.hpp> +#include <boost/optional.hpp> +#include <boost/noncopyable.hpp> +#include <iosfwd>                   // std::ostream +#include <stdexcept> + +#if LL_WINDOWS +#define WIN32_LEAN_AND_MEAN +#include <windows.h>                // HANDLE (eye roll) +#elif LL_LINUX +#if defined(Status) +#undef Status +#endif +#endif + +class LLEventPump; + +class LLProcess; +/// LLProcess instances are created on the heap by static factory methods and +/// managed by ref-counted pointers. +typedef boost::shared_ptr<LLProcess> LLProcessPtr; + +/** + * LLProcess handles launching an external process with specified command line + * arguments. It also keeps track of whether the process is still running, and + * can kill it if required. + * + * In discussing LLProcess, we use the term "parent" to refer to this process + * (the process invoking LLProcess), versus "child" to refer to the process + * spawned by LLProcess. + * + * LLProcess relies on periodic post() calls on the "mainloop" LLEventPump: an + * LLProcess object's Status won't update until the next "mainloop" tick. For + * instance, the Second Life viewer's main loop already posts to an + * LLEventPump by that name once per iteration. See + * indra/llcommon/tests/llprocess_test.cpp for an example of waiting for + * child-process termination in a standalone test context. + */ +class LL_COMMON_API LLProcess: public boost::noncopyable +{ +	LOG_CLASS(LLProcess); +public: +	/** +	 * Specify what to pass for each of child stdin, stdout, stderr. +	 * @see LLProcess::Params::files. +	 */ +	struct FileParam: public LLInitParam::Block<FileParam> +	{ +		/** +		 * type of file handle to pass to child process +		 * +		 * - "" (default): let the child inherit the same file handle used by +		 *   this process. For instance, if passed as stdout, child stdout +		 *   will be interleaved with stdout from this process. In this case, +		 *   @a name is moot and should be left "". +		 * +		 * - "file": open an OS filesystem file with the specified @a name. +		 *   <i>Not yet implemented.</i> +		 * +		 * - "pipe" or "tpipe" or "npipe": depends on @a name +		 * +		 *   - @a name.empty(): construct an OS pipe used only for this slot +		 *     of the forthcoming child process. +		 * +		 *   - ! @a name.empty(): in a global registry, find or create (using +		 *     the specified @a name) an OS pipe. The point of the (purely +		 *     internal) @a name is that passing the same @a name in more than +		 *     one slot for a given LLProcess -- or for slots in different +		 *     LLProcess instances -- means the same pipe. For example, you +		 *     might pass the same @a name value as both stdout and stderr to +		 *     make the child process produce both on the same actual pipe. Or +		 *     you might pass the same @a name as the stdout for one LLProcess +		 *     and the stdin for another to connect the two child processes. +		 *     Use LLProcess::getPipeName() to generate a unique name +		 *     guaranteed not to already exist in the registry. <i>Not yet +		 *     implemented.</i> +		 * +		 *   The difference between "pipe", "tpipe" and "npipe" is as follows. +		 * +		 *   - "pipe": direct LLProcess to monitor the parent end of the pipe, +		 *     pumping nonblocking I/O every frame. The expectation (at least +		 *     for stdout or stderr) is that the caller will listen for +		 *     incoming data and consume it as it arrives. It's important not +		 *     to neglect such a pipe, because it's buffered in memory. If you +		 *     suspect the child may produce a great volume of output between +		 *     frames, consider directing the child to write to a filesystem +		 *     file instead, then read the file later. +		 * +		 *   - "tpipe": do not engage LLProcess machinery to monitor the +		 *     parent end of the pipe. A "tpipe" is used only to connect +		 *     different child processes. As such, it makes little sense to +		 *     pass an empty @a name. <i>Not yet implemented.</i> +		 * +		 *   - "npipe": like "tpipe", but use an OS named pipe with a +		 *     generated name. Note that @a name is the @em internal name of +		 *     the pipe in our global registry -- it doesn't necessarily have +		 *     anything to do with the pipe's name in the OS filesystem. Use +		 *     LLProcess::getPipeName() to obtain the named pipe's OS +		 *     filesystem name, e.g. to pass it as the @a name to another +		 *     LLProcess instance using @a type "file". This supports usage +		 *     like bash's <(subcommand...) or >(subcommand...) +		 *     constructs. <i>Not yet implemented.</i> +		 * +		 * In all cases the open mode (read, write) is determined by the child +		 * slot you're filling. Child stdin means select the "read" end of a +		 * pipe, or open a filesystem file for reading; child stdout or stderr +		 * means select the "write" end of a pipe, or open a filesystem file +		 * for writing. +		 * +		 * Confusion such as passing the same pipe as the stdin of two +		 * processes (rather than stdout for one and stdin for the other) is +		 * explicitly permitted: it's up to the caller to construct meaningful +		 * LLProcess pipe graphs. +		 */ +		Optional<std::string> type; +		Optional<std::string> name; + +		FileParam(const std::string& tp="", const std::string& nm=""): +			type("type"), +			name("name") +		{ +			// If caller wants to specify values, use explicit assignment to +			// set them rather than initialization. +			if (! tp.empty()) type = tp; +			if (! nm.empty()) name = nm; +		} +	}; + +	/// Param block definition +	struct Params: public LLInitParam::Block<Params> +	{ +		Params(): +			executable("executable"), +			args("args"), +			cwd("cwd"), +			autokill("autokill", true), +			files("files"), +			postend("postend"), +			desc("desc") +		{} + +		/// pathname of executable +		Mandatory<std::string> executable; +		/** +		 * zero or more additional command-line arguments. Arguments are +		 * passed through as exactly as we can manage, whitespace and all. +		 * @note On Windows we manage this by implicitly double-quoting each +		 * argument while assembling the command line. +		 */ +		Multiple<std::string> args; +		/// current working directory, if need it changed +		Optional<std::string> cwd; +		/// implicitly kill process on destruction of LLProcess object +		/// (default true) +		Optional<bool> autokill; +		/** +		 * Up to three FileParam items: for child stdin, stdout, stderr. +		 * Passing two FileParam entries means default treatment for stderr, +		 * and so forth. +		 * +		 * @note LLInitParam::Block permits usage like this: +		 * @code +		 * LLProcess::Params params; +		 * ... +		 * params.files +		 *     .add(LLProcess::FileParam()) // stdin +		 *     .add(LLProcess::FileParam().type("pipe") // stdout +		 *     .add(LLProcess::FileParam().type("file").name("error.log")); +		 * @endcode +		 * +		 * @note While it's theoretically plausible to pass additional open +		 * file handles to a child specifically written to expect them, our +		 * underlying implementation doesn't yet support that. +		 */ +		Multiple<FileParam, AtMost<3> > files; +		/** +		 * On child-process termination, if this LLProcess object still +		 * exists, post LLSD event to LLEventPump with specified name (default +		 * no event). Event contains at least: +		 * +		 * - "id" as obtained from getProcessID() +		 * - "desc" short string description of child (executable + pid) +		 * - "state" @c state enum value, from Status.mState +		 * - "data"	 if "state" is EXITED, exit code; if KILLED, on Posix, +		 *   signal number +		 * - "string" English text describing "state" and "data" (e.g. "exited +		 *   with code 0") +		 */ +		Optional<std::string> postend; +		/** +		 * Description of child process for logging purposes. It need not be +		 * unique; the logged description string will contain the PID as well. +		 * If this is omitted, a description will be derived from the +		 * executable name. +		 */ +		Optional<std::string> desc; +	}; +	typedef LLSDParamAdapter<Params> LLSDOrParams; + +	/** +	 * Factory accepting either plain LLSD::Map or Params block. +	 * MAY RETURN DEFAULT-CONSTRUCTED LLProcessPtr if params invalid! +	 */ +	static LLProcessPtr create(const LLSDOrParams& params); +	virtual ~LLProcess(); + +	/// Is child process still running? +	bool isRunning() const; + +	/** +	 * State of child process +	 */ +	enum state +	{ +		UNSTARTED,					///< initial value, invisible to consumer +		RUNNING,					///< child process launched +		EXITED,						///< child process terminated voluntarily +		KILLED						///< child process terminated involuntarily +	}; + +	/** +	 * Status info +	 */ +	struct Status +	{ +		Status(): +			mState(UNSTARTED), +			mData(0) +		{} + +		state mState;				///< @see state +		/** +		 * - for mState == EXITED: mData is exit() code +		 * - for mState == KILLED: mData is signal number (Posix) +		 * - otherwise: mData is undefined +		 */ +		int mData; +	}; + +	/// Status query +	Status getStatus() const; +	/// English Status string query, for logging etc. +	std::string getStatusString() const; +	/// English Status string query for previously-captured Status +	std::string getStatusString(const Status& status) const; +	/// static English Status string query +	static std::string getStatusString(const std::string& desc, const Status& status); + +	// Attempt to kill the process -- returns true if the process is no longer running when it returns. +	// Note that even if this returns false, the process may exit some time after it's called. +	bool kill(const std::string& who=""); + +#if LL_WINDOWS +	typedef int id;                 ///< as returned by getProcessID() +	typedef HANDLE handle;          ///< as returned by getProcessHandle() +#else +	typedef pid_t id; +	typedef pid_t handle; +#endif +	/** +	 * Get an int-like id value. This is primarily intended for a human reader +	 * to differentiate processes. +	 */ +	id getProcessID() const; +	/** +	 * Get a "handle" of a kind that you might pass to platform-specific API +	 * functions to engage features not directly supported by LLProcess. +	 */ +	handle getProcessHandle() const; + +	/** +	 * Test if a process (@c handle obtained from getProcessHandle()) is still +	 * running. Return same nonzero @c handle value if still running, else +	 * zero, so you can test it like a bool. But if you want to update a +	 * stored variable as a side effect, you can write code like this: +	 * @code +	 * hchild = LLProcess::isRunning(hchild); +	 * @endcode +	 * @note This method is intended as a unit-test hook, not as the first of +	 * a whole set of operations supported on freestanding @c handle values. +	 * New functionality should be added as nonstatic members operating on +	 * the same data as getProcessHandle(). +	 * +	 * In particular, if child termination is detected by static isRunning() +	 * rather than by nonstatic isRunning(), the LLProcess object won't be +	 * aware of the child's changed status and may encounter OS errors trying +	 * to obtain it. static isRunning() is only intended for after the +	 * launching LLProcess object has been destroyed. +	 */ +	static handle isRunning(handle, const std::string& desc=""); + +	/// Provide symbolic access to child's file slots +	enum FILESLOT { STDIN=0, STDOUT=1, STDERR=2, NSLOTS=3 }; + +	/** +	 * For a pipe constructed with @a type "npipe", obtain the generated OS +	 * filesystem name for the specified pipe. Otherwise returns the empty +	 * string. @see LLProcess::FileParam::type +	 */ +	std::string getPipeName(FILESLOT) const; + +	/// base of ReadPipe, WritePipe +	class LL_COMMON_API BasePipe +	{ +	public: +		virtual ~BasePipe() = 0; + +		typedef std::size_t size_type; +		static const size_type npos; + +		/** +		 * Get accumulated buffer length. +		 * +		 * For WritePipe, is there still pending data to send to child? +		 * +		 * For ReadPipe, we often need to refrain from actually reading the +		 * std::istream returned by get_istream() until we've accumulated +		 * enough data to make it worthwhile. For instance, if we're expecting +		 * a number from the child, but the child happens to flush "12" before +		 * emitting "3\n", get_istream() >> myint could return 12 rather than +		 * 123! +		 */ +		virtual size_type size() const = 0; +	}; + +	/// As returned by getWritePipe() or getOptWritePipe() +	class WritePipe: public BasePipe +	{ +	public: +		/** +		 * Get ostream& on which to write to child's stdin. +		 * +		 * @usage +		 * @code +		 * myProcess->getWritePipe().get_ostream() << "Hello, child!" << std::endl; +		 * @endcode +		 */ +		virtual std::ostream& get_ostream() = 0; +	}; + +	/// As returned by getReadPipe() or getOptReadPipe() +	class ReadPipe: public BasePipe +	{ +	public: +		/** +		 * Get istream& on which to read from child's stdout or stderr. +		 * +		 * @usage +		 * @code +		 * std::string stuff; +		 * myProcess->getReadPipe().get_istream() >> stuff; +		 * @endcode +		 * +		 * You should be sure in advance that the ReadPipe in question can +		 * fill the request. @see getPump() +		 */ +		virtual std::istream& get_istream() = 0; + +		/** +		 * Like std::getline(get_istream(), line), but trims off trailing '\r' +		 * to make calling code less platform-sensitive. +		 */ +		virtual std::string getline() = 0; + +		/** +		 * Like get_istream().read(buffer, n), but returns std::string rather +		 * than requiring caller to construct a buffer, etc. +		 */ +		virtual std::string read(size_type len) = 0; + +		/** +		 * Peek at accumulated buffer data without consuming it. Optional +		 * parameters give you substr() functionality. +		 * +		 * @note You can discard buffer data using get_istream().ignore(n). +		 */ +		virtual std::string peek(size_type offset=0, size_type len=npos) const = 0; + +		/** +		 * Detect presence of a substring (or char) in accumulated buffer data +		 * without retrieving it. Optional offset allows you to search from +		 * specified position. +		 */ +		template <typename SEEK> +		bool contains(SEEK seek, size_type offset=0) const +		{ return find(seek, offset) != npos; } + +		/** +		 * Search for a substring in accumulated buffer data without +		 * retrieving it. Returns size_type position at which found, or npos +		 * meaning not found. Optional offset allows you to search from +		 * specified position. +		 */ +		virtual size_type find(const std::string& seek, size_type offset=0) const = 0; + +		/** +		 * Search for a char in accumulated buffer data without retrieving it. +		 * Returns size_type position at which found, or npos meaning not +		 * found. Optional offset allows you to search from specified +		 * position. +		 */ +		virtual size_type find(char seek, size_type offset=0) const = 0; + +		/** +		 * Get LLEventPump& on which to listen for incoming data. The posted +		 * LLSD::Map event will contain: +		 * +		 * - "data" part of pending data; see setLimit() +		 * - "len" entire length of pending data, regardless of setLimit() +		 * - "slot" this ReadPipe's FILESLOT, e.g. LLProcess::STDOUT +		 * - "name" e.g. "stdout" +		 * - "desc" e.g. "SLPlugin (pid) stdout" +		 * - "eof" @c true means there no more data will arrive on this pipe, +		 *   therefore no more events on this pump +		 * +		 * If the child sends "abc", and this ReadPipe posts "data"="abc", but +		 * you don't consume it by reading the std::istream returned by +		 * get_istream(), and the child next sends "def", ReadPipe will post +		 * "data"="abcdef". +		 */ +		virtual LLEventPump& getPump() = 0; + +		/** +		 * Set maximum length of buffer data that will be posted in the LLSD +		 * announcing arrival of new data from the child. If you call +		 * setLimit(5), and the child sends "abcdef", the LLSD event will +		 * contain "data"="abcde". However, you may still read the entire +		 * "abcdef" from get_istream(): this limit affects only the size of +		 * the data posted with the LLSD event. If you don't call this method, +		 * @em no data will be posted: the default is 0 bytes. +		 */ +		virtual void setLimit(size_type limit) = 0; + +		/** +		 * Query the current setLimit() limit. +		 */ +		virtual size_type getLimit() const = 0; +	}; + +	/// Exception thrown by getWritePipe(), getReadPipe() if you didn't ask to +	/// create a pipe at the corresponding FILESLOT. +	struct NoPipe: public std::runtime_error +	{ +		NoPipe(const std::string& what): std::runtime_error(what) {} +	}; + +	/** +	 * Get a reference to the (only) WritePipe for this LLProcess. @a slot, if +	 * specified, must be STDIN. Throws NoPipe if you did not request a "pipe" +	 * for child stdin. Use this method when you know how you created the +	 * LLProcess in hand. +	 */ +	WritePipe& getWritePipe(FILESLOT slot=STDIN); + +	/** +	 * Get a boost::optional<WritePipe&> to the (only) WritePipe for this +	 * LLProcess. @a slot, if specified, must be STDIN. The return value is +	 * empty if you did not request a "pipe" for child stdin. Use this method +	 * for inspecting an LLProcess you did not create. +	 */ +	boost::optional<WritePipe&> getOptWritePipe(FILESLOT slot=STDIN); + +	/** +	 * Get a reference to one of the ReadPipes for this LLProcess. @a slot, if +	 * specified, must be STDOUT or STDERR. Throws NoPipe if you did not +	 * request a "pipe" for child stdout or stderr. Use this method when you +	 * know how you created the LLProcess in hand. +	 */ +	ReadPipe& getReadPipe(FILESLOT slot); + +	/** +	 * Get a boost::optional<ReadPipe&> to one of the ReadPipes for this +	 * LLProcess. @a slot, if specified, must be STDOUT or STDERR. The return +	 * value is empty if you did not request a "pipe" for child stdout or +	 * stderr. Use this method for inspecting an LLProcess you did not create. +	 */ +	boost::optional<ReadPipe&> getOptReadPipe(FILESLOT slot); + +	/// little utilities that really should already be somewhere else in the +	/// code base +	static std::string basename(const std::string& path); +	static std::string getline(std::istream&); + +private: +	/// constructor is private: use create() instead +	LLProcess(const LLSDOrParams& params); +	void autokill(); +	// Classic-C-style APR callback +	static void status_callback(int reason, void* data, int status); +	// Object-oriented callback +	void handle_status(int reason, int status); +	// implementation for get[Opt][Read|Write]Pipe() +	template <class PIPETYPE> +	PIPETYPE& getPipe(FILESLOT slot); +	template <class PIPETYPE> +	boost::optional<PIPETYPE&> getOptPipe(FILESLOT slot); +	template <class PIPETYPE> +	PIPETYPE* getPipePtr(std::string& error, FILESLOT slot); + +	std::string mDesc; +	std::string mPostend; +	apr_proc_t mProcess; +	bool mAutokill; +	Status mStatus; +	// explicitly want this ptr_vector to be able to store NULLs +	typedef boost::ptr_vector< boost::nullable<BasePipe> > PipeVector; +	PipeVector mPipes; +}; + +/// for logging +LL_COMMON_API std::ostream& operator<<(std::ostream&, const LLProcess::Params&); + +#endif // LL_LLPROCESS_H diff --git a/indra/llcommon/llprocesslauncher.cpp b/indra/llcommon/llprocesslauncher.cpp deleted file mode 100644 index 10950181fd..0000000000 --- a/indra/llcommon/llprocesslauncher.cpp +++ /dev/null @@ -1,357 +0,0 @@ -/**  - * @file llprocesslauncher.cpp - * @brief Utility class for launching, terminating, and tracking the state of processes. - * - * $LicenseInfo:firstyear=2008&license=viewerlgpl$ - * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. - *  - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; - * version 2.1 of the License only. - *  - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU - * Lesser General Public License for more details. - *  - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA - *  - * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA - * $/LicenseInfo$ - */ -  -#include "linden_common.h" - -#include "llprocesslauncher.h" - -#include <iostream> -#if LL_DARWIN || LL_LINUX -// not required or present on Win32 -#include <sys/wait.h> -#endif - -LLProcessLauncher::LLProcessLauncher() -{ -#if LL_WINDOWS -	mProcessHandle = 0; -#else -	mProcessID = 0; -#endif -} - -LLProcessLauncher::~LLProcessLauncher() -{ -	kill(); -} - -void LLProcessLauncher::setExecutable(const std::string &executable) -{ -	mExecutable = executable; -} - -void LLProcessLauncher::setWorkingDirectory(const std::string &dir) -{ -	mWorkingDir = dir; -} - -const std::string& LLProcessLauncher::getExecutable() const -{ -	return mExecutable; -} - -void LLProcessLauncher::clearArguments() -{ -	mLaunchArguments.clear(); -} - -void LLProcessLauncher::addArgument(const std::string &arg) -{ -	mLaunchArguments.push_back(arg); -} - -void LLProcessLauncher::addArgument(const char *arg) -{ -	mLaunchArguments.push_back(std::string(arg)); -} - -#if LL_WINDOWS - -int LLProcessLauncher::launch(void) -{ -	// If there was already a process associated with this object, kill it. -	kill(); -	orphan(); - -	int result = 0; -	 -	PROCESS_INFORMATION pinfo; -	STARTUPINFOA sinfo; -	memset(&sinfo, 0, sizeof(sinfo)); -	 -	std::string args = mExecutable; -	for(int i = 0; i < (int)mLaunchArguments.size(); i++) -	{ -		args += " "; -		args += mLaunchArguments[i]; -	} -	 -	// So retarded.  Windows requires that the second parameter to CreateProcessA be a writable (non-const) string... -	char *args2 = new char[args.size() + 1]; -	strcpy(args2, args.c_str()); - -	const char * working_directory = 0; -	if(!mWorkingDir.empty()) working_directory = mWorkingDir.c_str(); -	if( ! CreateProcessA( NULL, args2, NULL, NULL, FALSE, 0, NULL, working_directory, &sinfo, &pinfo ) ) -	{ -		result = GetLastError(); - -		LPTSTR error_str = 0; -		if( -			FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, -				NULL, -				result, -				0, -				(LPTSTR)&error_str, -				0, -				NULL)  -			!= 0)  -		{ -			char message[256]; -			wcstombs(message, error_str, 256); -			message[255] = 0; -			llwarns << "CreateProcessA failed: " << message << llendl; -			LocalFree(error_str); -		} - -		if(result == 0) -		{ -			// Make absolutely certain we return a non-zero value on failure. -			result = -1; -		} -	} -	else -	{ -		// foo = pinfo.dwProcessId; // get your pid here if you want to use it later on -		// CloseHandle(pinfo.hProcess); // stops leaks - nothing else -		mProcessHandle = pinfo.hProcess; -		CloseHandle(pinfo.hThread); // stops leaks - nothing else -	}		 -	 -	delete[] args2; -	 -	return result; -} - -bool LLProcessLauncher::isRunning(void) -{ -	if(mProcessHandle != 0)		 -	{ -		DWORD waitresult = WaitForSingleObject(mProcessHandle, 0); -		if(waitresult == WAIT_OBJECT_0) -		{ -			// the process has completed. -			mProcessHandle = 0; -		}			 -	} - -	return (mProcessHandle != 0); -} -bool LLProcessLauncher::kill(void) -{ -	bool result = true; -	 -	if(mProcessHandle != 0) -	{ -		TerminateProcess(mProcessHandle,0); - -		if(isRunning()) -		{ -			result = false; -		} -	} -	 -	return result; -} - -void LLProcessLauncher::orphan(void) -{ -	// Forget about the process -	mProcessHandle = 0; -} - -// static  -void LLProcessLauncher::reap(void) -{ -	// No actions necessary on Windows. -} - -#else // Mac and linux - -#include <signal.h> -#include <fcntl.h> -#include <errno.h> - -static std::list<pid_t> sZombies; - -// Attempt to reap a process ID -- returns true if the process has exited and been reaped, false otherwise. -static bool reap_pid(pid_t pid) -{ -	bool result = false; -	 -	pid_t wait_result = ::waitpid(pid, NULL, WNOHANG); -	if(wait_result == pid) -	{ -		result = true; -	} -	else if(wait_result == -1) -	{ -		if(errno == ECHILD) -		{ -			// No such process -- this may mean we're ignoring SIGCHILD. -			result = true; -		} -	} -	 -	return result; -} - -int LLProcessLauncher::launch(void) -{ -	// If there was already a process associated with this object, kill it. -	kill(); -	orphan(); -	 -	int result = 0; -	int current_wd = -1; -	 -	// create an argv vector for the child process -	const char ** fake_argv = new const char *[mLaunchArguments.size() + 2];  // 1 for the executable path, 1 for the NULL terminator - -	int i = 0; -	 -	// add the executable path -	fake_argv[i++] = mExecutable.c_str(); -	 -	// and any arguments -	for(int j=0; j < mLaunchArguments.size(); j++) -		fake_argv[i++] = mLaunchArguments[j].c_str(); -	 -	// terminate with a null pointer -	fake_argv[i] = NULL; -	 -	if(!mWorkingDir.empty()) -	{ -		// save the current working directory -		current_wd = ::open(".", O_RDONLY); -	 -		// and change to the one the child will be executed in -		if (::chdir(mWorkingDir.c_str())) -		{ -			// chdir failed -		} -	} -		 - 	// flush all buffers before the child inherits them - 	::fflush(NULL); - -	pid_t id = vfork(); -	if(id == 0) -	{ -		// child process -		 -		::execv(mExecutable.c_str(), (char * const *)fake_argv); -		 -		// If we reach this point, the exec failed. -		// Use _exit() instead of exit() per the vfork man page. -		_exit(0); -	} - -	// parent process -	 -	if(current_wd >= 0) -	{ -		// restore the previous working directory -		if (::fchdir(current_wd)) -		{ -			// chdir failed -		} -		::close(current_wd); -	} -	 -	delete[] fake_argv; -	 -	mProcessID = id; - -	return result; -} - -bool LLProcessLauncher::isRunning(void) -{ -	if(mProcessID != 0) -	{ -		// Check whether the process has exited, and reap it if it has. -		if(reap_pid(mProcessID)) -		{ -			// the process has exited. -			mProcessID = 0; -		} -	} -	 -	return (mProcessID != 0); -} - -bool LLProcessLauncher::kill(void) -{ -	bool result = true; -	 -	if(mProcessID != 0) -	{ -		// Try to kill the process.  We'll do approximately the same thing whether the kill returns an error or not, so we ignore the result. -		(void)::kill(mProcessID, SIGTERM); -		 -		// This will have the side-effect of reaping the zombie if the process has exited. -		if(isRunning()) -		{ -			result = false; -		} -	} -	 -	return result; -} - -void LLProcessLauncher::orphan(void) -{ -	// Disassociate the process from this object -	if(mProcessID != 0) -	{	 -		// We may still need to reap the process's zombie eventually -		sZombies.push_back(mProcessID); -	 -		mProcessID = 0; -	} -} - -// static  -void LLProcessLauncher::reap(void) -{ -	// Attempt to real all saved process ID's. -	 -	std::list<pid_t>::iterator iter = sZombies.begin(); -	while(iter != sZombies.end()) -	{ -		if(reap_pid(*iter)) -		{ -			iter = sZombies.erase(iter); -		} -		else -		{ -			iter++; -		} -	} -} - -#endif diff --git a/indra/llcommon/llprocesslauncher.h b/indra/llcommon/llprocesslauncher.h deleted file mode 100644 index 954c249147..0000000000 --- a/indra/llcommon/llprocesslauncher.h +++ /dev/null @@ -1,90 +0,0 @@ -/**  - * @file llprocesslauncher.h - * @brief Utility class for launching, terminating, and tracking the state of processes. - * - * $LicenseInfo:firstyear=2008&license=viewerlgpl$ - * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. - *  - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; - * version 2.1 of the License only. - *  - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU - * Lesser General Public License for more details. - *  - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA - *  - * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA - * $/LicenseInfo$ - */ -  -#ifndef LL_LLPROCESSLAUNCHER_H -#define LL_LLPROCESSLAUNCHER_H - -#if LL_WINDOWS -#include <windows.h> -#endif - - -/* -	LLProcessLauncher handles launching external processes with specified command line arguments. -	It also keeps track of whether the process is still running, and can kill it if required. -*/ - -class LL_COMMON_API LLProcessLauncher -{ -	LOG_CLASS(LLProcessLauncher); -public: -	LLProcessLauncher(); -	virtual ~LLProcessLauncher(); -	 -	void setExecutable(const std::string &executable); -	void setWorkingDirectory(const std::string &dir); - -	const std::string& getExecutable() const; - -	void clearArguments(); -	void addArgument(const std::string &arg); -	void addArgument(const char *arg); -		 -	int launch(void); -	bool isRunning(void); -	 -	// Attempt to kill the process -- returns true if the process is no longer running when it returns. -	// Note that even if this returns false, the process may exit some time after it's called. -	bool kill(void); -	 -	// Use this if you want the external process to continue execution after the LLProcessLauncher instance controlling it is deleted. -	// Normally, the destructor will attempt to kill the process and wait for termination. -	// This should only be used if the viewer is about to exit -- otherwise, the child process will become a zombie after it exits. -	void orphan(void);	 -	 -	// This needs to be called periodically on Mac/Linux to clean up zombie processes. -	static void reap(void); -	 -	// Accessors for platform-specific process ID -#if LL_WINDOWS -	HANDLE getProcessHandle() { return mProcessHandle; }; -#else -	pid_t getProcessID() { return mProcessID; }; -#endif	 -	 -private: -	std::string mExecutable; -	std::string mWorkingDir; -	std::vector<std::string> mLaunchArguments; -	 -#if LL_WINDOWS -	HANDLE mProcessHandle; -#else -	pid_t mProcessID; -#endif -}; - -#endif // LL_LLPROCESSLAUNCHER_H diff --git a/indra/llcommon/llqueuedthread.cpp b/indra/llcommon/llqueuedthread.cpp index 5dee7a3541..1738c16dea 100644 --- a/indra/llcommon/llqueuedthread.cpp +++ b/indra/llcommon/llqueuedthread.cpp @@ -109,7 +109,7 @@ void LLQueuedThread::shutdown()  // MAIN THREAD  // virtual -S32 LLQueuedThread::update(U32 max_time_ms) +S32 LLQueuedThread::update(F32 max_time_ms)  {  	if (!mStarted)  	{ @@ -122,7 +122,7 @@ S32 LLQueuedThread::update(U32 max_time_ms)  	return updateQueue(max_time_ms);  } -S32 LLQueuedThread::updateQueue(U32 max_time_ms) +S32 LLQueuedThread::updateQueue(F32 max_time_ms)  {  	F64 max_time = (F64)max_time_ms * .001;  	LLTimer timer; diff --git a/indra/llcommon/llqueuedthread.h b/indra/llcommon/llqueuedthread.h index 499d13a792..d3704b0fe2 100644 --- a/indra/llcommon/llqueuedthread.h +++ b/indra/llcommon/llqueuedthread.h @@ -173,8 +173,8 @@ protected:  public:  	bool waitForResult(handle_t handle, bool auto_complete = true); -	virtual S32 update(U32 max_time_ms); -	S32 updateQueue(U32 max_time_ms); +	virtual S32 update(F32 max_time_ms); +	S32 updateQueue(F32 max_time_ms);  	void waitOnPending();  	void printQueueStats(); diff --git a/indra/llcommon/llstreamqueue.cpp b/indra/llcommon/llstreamqueue.cpp new file mode 100644 index 0000000000..1116a2b6a2 --- /dev/null +++ b/indra/llcommon/llstreamqueue.cpp @@ -0,0 +1,24 @@ +/** + * @file   llstreamqueue.cpp + * @author Nat Goodspeed + * @date   2012-01-05 + * @brief  Implementation for llstreamqueue. + *  + * $LicenseInfo:firstyear=2012&license=viewerlgpl$ + * Copyright (c) 2012, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "llstreamqueue.h" +// STL headers +// std headers +// external library headers +// other Linden headers + +// As of this writing, llstreamqueue.h is entirely template-based, therefore +// we don't strictly need a corresponding .cpp file. However, our CMake test +// macro assumes one. Here it is. +bool llstreamqueue_cpp_ignored = true; diff --git a/indra/llcommon/llstreamqueue.h b/indra/llcommon/llstreamqueue.h new file mode 100644 index 0000000000..0726bad175 --- /dev/null +++ b/indra/llcommon/llstreamqueue.h @@ -0,0 +1,240 @@ +/** + * @file   llstreamqueue.h + * @author Nat Goodspeed + * @date   2012-01-04 + * @brief  Definition of LLStreamQueue + *  + * $LicenseInfo:firstyear=2012&license=viewerlgpl$ + * Copyright (c) 2012, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_LLSTREAMQUEUE_H) +#define LL_LLSTREAMQUEUE_H + +#include <string> +#include <list> +#include <iosfwd>                   // std::streamsize +#include <boost/iostreams/categories.hpp> + +/** + * This class is a growable buffer between a producer and consumer. It serves + * as a queue usable with Boost.Iostreams -- hence, a "stream queue." + * + * This is especially useful for buffering nonblocking I/O. For instance, we + * want application logic to be able to serialize LLSD to a std::ostream. We + * may write more data than the destination pipe can handle all at once, but + * it's imperative NOT to block the application-level serialization call. So + * we buffer it instead. Successive frames can try nonblocking writes to the + * destination pipe until all buffered data has been sent. + * + * Similarly, we want application logic be able to deserialize LLSD from a + * std::istream. Again, we must not block that deserialize call waiting for + * more data to arrive from the input pipe! Instead we build up a buffer over + * a number of frames, using successive nonblocking reads, until we have + * "enough" data to be able to present it through a std::istream. + * + * @note The use cases for this class overlap somewhat with those for the + * LLIOPipe/LLPumpIO hierarchies, and indeed we considered using those. This + * class has two virtues over the older machinery: + * + * # It's vastly simpler -- way fewer concepts. It's not clear to me whether + *   there were ever LLIOPipe/etc. use cases that demanded all the fanciness + *   rolled in, or whether they were simply overdesigned. In any case, no + *   remaining Lindens will admit to familiarity with those classes -- and + *   they're sufficiently obtuse that it would take considerable learning + *   curve to figure out how to use them properly. The bottom line is that + *   current management is not keen on any more engineers climbing that curve. + * # This class is designed around available components such as std::string, + *   std::list, Boost.Iostreams. There's less proprietary code. + */ +template <typename Ch> +class LLGenericStreamQueue +{ +public: +    LLGenericStreamQueue(): +        mSize(0), +        mClosed(false) +    {} + +    /** +     * Boost.Iostreams Source Device facade for use with other Boost.Iostreams +     * functionality. LLGenericStreamQueue doesn't quite fit any of the Boost +     * 1.48 Iostreams concepts; instead it behaves as both a Sink and a +     * Source. This is its Source facade. +     */ +    struct Source +    { +        typedef Ch char_type; +        typedef boost::iostreams::source_tag category; + +        /// Bind the underlying LLGenericStreamQueue +        Source(LLGenericStreamQueue& sq): +            mStreamQueue(sq) +        {} + +        // Read up to n characters from the underlying data source into the +        // buffer s, returning the number of characters read; return -1 to +        // indicate EOF +        std::streamsize read(Ch* s, std::streamsize n) +        { +            return mStreamQueue.read(s, n); +        } + +        LLGenericStreamQueue& mStreamQueue; +    }; + +    /** +     * Boost.Iostreams Sink Device facade for use with other Boost.Iostreams +     * functionality. LLGenericStreamQueue doesn't quite fit any of the Boost +     * 1.48 Iostreams concepts; instead it behaves as both a Sink and a +     * Source. This is its Sink facade. +     */ +    struct Sink +    { +        typedef Ch char_type; +        typedef boost::iostreams::sink_tag category; + +        /// Bind the underlying LLGenericStreamQueue +        Sink(LLGenericStreamQueue& sq): +            mStreamQueue(sq) +        {} + +        /// Write up to n characters from the buffer s to the output sequence, +        /// returning the number of characters written +        std::streamsize write(const Ch* s, std::streamsize n) +        { +            return mStreamQueue.write(s, n); +        } + +        /// Send EOF to consumer +        void close() +        { +            mStreamQueue.close(); +        } + +        LLGenericStreamQueue& mStreamQueue; +    }; + +    /// Present Boost.Iostreams Source facade +    Source asSource() { return Source(*this); } +    /// Present Boost.Iostreams Sink facade +    Sink   asSink()   { return Sink(*this); } + +    /// append data to buffer +    std::streamsize write(const Ch* s, std::streamsize n) +    { +        // Unclear how often we might be asked to write 0 bytes -- perhaps a +        // naive caller responding to an unready nonblocking read. But if we +        // do get such a call, don't add a completely empty BufferList entry. +        if (n == 0) +            return n; +        // We could implement this using a single std::string object, a la +        // ostringstream. But the trouble with appending to a string is that +        // you might have to recopy all previous contents to grow its size. If +        // we want this to scale to large data volumes, better to allocate +        // individual pieces. +        mBuffer.push_back(string(s, n)); +        mSize += n; +        return n; +    } + +    /** +     * Inform this LLGenericStreamQueue that no further data are forthcoming. +     * For our purposes, close() is strictly a producer-side operation; +     * there's little point in closing the consumer side. +     */ +    void close() +    { +        mClosed = true; +    } + +    /// consume data from buffer +    std::streamsize read(Ch* s, std::streamsize n) +    { +        // read() is actually a convenience method for peek() followed by +        // skip(). +        std::streamsize got(peek(s, n)); +        // We can only skip() as many characters as we can peek(); ignore +        // skip() return here. +        skip(n); +        return got; +    } + +    /// Retrieve data from buffer without consuming. Like read(), return -1 on +    /// EOF. +    std::streamsize peek(Ch* s, std::streamsize n) const; + +    /// Consume data from buffer without retrieving. Unlike read() and peek(), +    /// at EOF we simply skip 0 characters. +    std::streamsize skip(std::streamsize n); + +    /// How many characters do we currently have buffered? +    std::streamsize size() const +    { +        return mSize; +    } + +private: +    typedef std::basic_string<Ch> string; +    typedef std::list<string> BufferList; +    BufferList mBuffer; +    std::streamsize mSize; +    bool mClosed; +}; + +template <typename Ch> +std::streamsize LLGenericStreamQueue<Ch>::peek(Ch* s, std::streamsize n) const +{ +    // Here we may have to build up 'n' characters from an arbitrary +    // number of individual BufferList entries. +    typename BufferList::const_iterator bli(mBuffer.begin()), blend(mBuffer.end()); +    // Indicate EOF if producer has closed the pipe AND we've exhausted +    // all previously-buffered data. +    if (mClosed && bli == blend) +    { +        return -1; +    } +    // Here either producer hasn't yet closed, or we haven't yet exhausted +    // remaining data. +    std::streamsize needed(n), got(0); +    // Loop until either we run out of BufferList entries or we've +    // completely satisfied the request. +    for ( ; bli != blend && needed; ++bli) +    { +        std::streamsize chunk(std::min(needed, std::streamsize(bli->length()))); +        std::copy(bli->begin(), bli->begin() + chunk, s); +        needed -= chunk; +        s      += chunk; +        got    += chunk; +    } +    return got; +} + +template <typename Ch> +std::streamsize LLGenericStreamQueue<Ch>::skip(std::streamsize n) +{ +    typename BufferList::iterator bli(mBuffer.begin()), blend(mBuffer.end()); +    std::streamsize toskip(n), skipped(0); +    while (bli != blend && toskip >= bli->length()) +    { +        std::streamsize chunk(bli->length()); +        typename BufferList::iterator zap(bli++); +        mBuffer.erase(zap); +        mSize   -= chunk; +        toskip  -= chunk; +        skipped += chunk; +    } +    if (bli != blend && toskip) +    { +        bli->erase(bli->begin(), bli->begin() + toskip); +        mSize   -= toskip; +        skipped += toskip; +    } +    return skipped; +} + +typedef LLGenericStreamQueue<char>    LLStreamQueue; +typedef LLGenericStreamQueue<wchar_t> LLWStreamQueue; + +#endif /* ! defined(LL_LLSTREAMQUEUE_H) */ diff --git a/indra/llcommon/llstring.cpp b/indra/llcommon/llstring.cpp index e7fe656808..fa0eb9f72c 100644 --- a/indra/llcommon/llstring.cpp +++ b/indra/llcommon/llstring.cpp @@ -912,22 +912,24 @@ S32 LLStringUtil::format(std::string& s, const format_map_t& substitutions);  template<>   void LLStringUtil::getTokens(const std::string& instr, std::vector<std::string >& tokens, const std::string& delims)  { -	std::string currToken; -	std::string::size_type begIdx, endIdx; - -	begIdx = instr.find_first_not_of (delims); -	while (begIdx != std::string::npos) +	// Starting at offset 0, scan forward for the next non-delimiter. We're +	// done when the only characters left in 'instr' are delimiters. +	for (std::string::size_type begIdx, endIdx = 0; +		 (begIdx = instr.find_first_not_of (delims, endIdx)) != std::string::npos; )  	{ +		// Found a non-delimiter. After that, find the next delimiter.  		endIdx = instr.find_first_of (delims, begIdx);  		if (endIdx == std::string::npos)  		{ +			// No more delimiters: this token extends to the end of the string.  			endIdx = instr.length();  		} -		currToken = instr.substr(begIdx, endIdx - begIdx); +		// extract the token between begIdx and endIdx; substr() needs length +		std::string currToken(instr.substr(begIdx, endIdx - begIdx));  		LLStringUtil::trim (currToken);  		tokens.push_back(currToken); -		begIdx = instr.find_first_not_of (delims, endIdx); +		// next scan past delimiters starts at endIdx  	}  } diff --git a/indra/llcommon/llstring.h b/indra/llcommon/llstring.h index 7e41e787b5..09733e8e2a 100644 --- a/indra/llcommon/llstring.h +++ b/indra/llcommon/llstring.h @@ -40,6 +40,7 @@  #endif  #include <string.h> +#include <boost/scoped_ptr.hpp>  #if LL_SOLARIS  // stricmp and strnicmp do not exist on Solaris: @@ -237,40 +238,77 @@ private:  	static std::string sLocale;  public: -	typedef typename std::basic_string<T>::size_type size_type; +	typedef std::basic_string<T> string_type; +	typedef typename string_type::size_type size_type;  public:  	/////////////////////////////////////////////////////////////////////////////////////////  	// Static Utility functions that operate on std::strings -	static const std::basic_string<T> null; +	static const string_type null;  	typedef std::map<LLFormatMapString, LLFormatMapString> format_map_t; -	LL_COMMON_API static void getTokens(const std::basic_string<T>& instr, std::vector<std::basic_string<T> >& tokens, const std::basic_string<T>& delims); -	LL_COMMON_API static void formatNumber(std::basic_string<T>& numStr, std::basic_string<T> decimals); -	LL_COMMON_API static bool formatDatetime(std::basic_string<T>& replacement, std::basic_string<T> token, std::basic_string<T> param, S32 secFromEpoch); -	LL_COMMON_API static S32 format(std::basic_string<T>& s, const format_map_t& substitutions); -	LL_COMMON_API static S32 format(std::basic_string<T>& s, const LLSD& substitutions); -	LL_COMMON_API static bool simpleReplacement(std::basic_string<T>& replacement, std::basic_string<T> token, const format_map_t& substitutions); -	LL_COMMON_API static bool simpleReplacement(std::basic_string<T>& replacement, std::basic_string<T> token, const LLSD& substitutions); +	/// considers any sequence of delims as a single field separator +	LL_COMMON_API static void getTokens(const string_type& instr, +										std::vector<string_type >& tokens, +										const string_type& delims); +	/// like simple scan overload, but returns scanned vector +	static std::vector<string_type> getTokens(const string_type& instr, +											  const string_type& delims); +	/// add support for keep_delims and quotes (either could be empty string) +	static void getTokens(const string_type& instr, +						  std::vector<string_type>& tokens, +						  const string_type& drop_delims, +						  const string_type& keep_delims, +						  const string_type& quotes=string_type()); +	/// like keep_delims-and-quotes overload, but returns scanned vector +	static std::vector<string_type> getTokens(const string_type& instr, +											  const string_type& drop_delims, +											  const string_type& keep_delims, +											  const string_type& quotes=string_type()); +	/// add support for escapes (could be empty string) +	static void getTokens(const string_type& instr, +						  std::vector<string_type>& tokens, +						  const string_type& drop_delims, +						  const string_type& keep_delims, +						  const string_type& quotes, +						  const string_type& escapes); +	/// like escapes overload, but returns scanned vector +	static std::vector<string_type> getTokens(const string_type& instr, +											  const string_type& drop_delims, +											  const string_type& keep_delims, +											  const string_type& quotes, +											  const string_type& escapes); + +	LL_COMMON_API static void formatNumber(string_type& numStr, string_type decimals); +	LL_COMMON_API static bool formatDatetime(string_type& replacement, string_type token, string_type param, S32 secFromEpoch); +	LL_COMMON_API static S32 format(string_type& s, const format_map_t& substitutions); +	LL_COMMON_API static S32 format(string_type& s, const LLSD& substitutions); +	LL_COMMON_API static bool simpleReplacement(string_type& replacement, string_type token, const format_map_t& substitutions); +	LL_COMMON_API static bool simpleReplacement(string_type& replacement, string_type token, const LLSD& substitutions);  	LL_COMMON_API static void setLocale (std::string inLocale);  	LL_COMMON_API static std::string getLocale (void); -	static bool isValidIndex(const std::basic_string<T>& string, size_type i) +	static bool isValidIndex(const string_type& string, size_type i)  	{  		return !string.empty() && (0 <= i) && (i <= string.size());  	} -	static void	trimHead(std::basic_string<T>& string); -	static void	trimTail(std::basic_string<T>& string); -	static void	trim(std::basic_string<T>& string)	{ trimHead(string); trimTail(string); } -	static void truncate(std::basic_string<T>& string, size_type count); +	static bool contains(const string_type& string, T c, size_type i=0) +	{ +		return string.find(c, i) != string_type::npos; +	} -	static void	toUpper(std::basic_string<T>& string); -	static void	toLower(std::basic_string<T>& string); +	static void	trimHead(string_type& string); +	static void	trimTail(string_type& string); +	static void	trim(string_type& string)	{ trimHead(string); trimTail(string); } +	static void truncate(string_type& string, size_type count); + +	static void	toUpper(string_type& string); +	static void	toLower(string_type& string);  	// True if this is the head of s. -	static BOOL	isHead( const std::basic_string<T>& string, const T* s );  +	static BOOL	isHead( const string_type& string, const T* s );   	/**  	 * @brief Returns true if string starts with substr @@ -278,8 +316,8 @@ public:  	 * If etither string or substr are empty, this method returns false.  	 */  	static bool startsWith( -		const std::basic_string<T>& string, -		const std::basic_string<T>& substr); +		const string_type& string, +		const string_type& substr);  	/**  	 * @brief Returns true if string ends in substr @@ -287,19 +325,32 @@ public:  	 * If etither string or substr are empty, this method returns false.  	 */  	static bool endsWith( -		const std::basic_string<T>& string, -		const std::basic_string<T>& substr); +		const string_type& string, +		const string_type& substr); -	static void	addCRLF(std::basic_string<T>& string); -	static void	removeCRLF(std::basic_string<T>& string); +	static void	addCRLF(string_type& string); +	static void	removeCRLF(string_type& string); -	static void	replaceTabsWithSpaces( std::basic_string<T>& string, size_type spaces_per_tab ); -	static void	replaceNonstandardASCII( std::basic_string<T>& string, T replacement ); -	static void	replaceChar( std::basic_string<T>& string, T target, T replacement ); -	static void replaceString( std::basic_string<T>& string, std::basic_string<T> target, std::basic_string<T> replacement ); +	static void	replaceTabsWithSpaces( string_type& string, size_type spaces_per_tab ); +	static void	replaceNonstandardASCII( string_type& string, T replacement ); +	static void	replaceChar( string_type& string, T target, T replacement ); +	static void replaceString( string_type& string, string_type target, string_type replacement ); -	static BOOL	containsNonprintable(const std::basic_string<T>& string); -	static void	stripNonprintable(std::basic_string<T>& string); +	static BOOL	containsNonprintable(const string_type& string); +	static void	stripNonprintable(string_type& string); + +	/** +	 * Double-quote an argument string if needed, unless it's already +	 * double-quoted. Decide whether it's needed based on the presence of any +	 * character in @a triggers (default space or double-quote). If we quote +	 * it, escape any embedded double-quote with the @a escape string (default +	 * backslash). +	 * +	 * Passing triggers="" means always quote, unless it's already double-quoted. +	 */ +	static string_type quote(const string_type& str, +							 const string_type& triggers=" \"", +							 const string_type& escape="\\");  	/**  	 * @brief Unsafe way to make ascii characters. You should probably @@ -308,18 +359,18 @@ public:  	 * The 2 and 4 byte std::string probably work, so LLWStringUtil::_makeASCII  	 * should work.  	 */ -	static void _makeASCII(std::basic_string<T>& string); +	static void _makeASCII(string_type& string);  	// Conversion to other data types -	static BOOL	convertToBOOL(const std::basic_string<T>& string, BOOL& value); -	static BOOL	convertToU8(const std::basic_string<T>& string, U8& value); -	static BOOL	convertToS8(const std::basic_string<T>& string, S8& value); -	static BOOL	convertToS16(const std::basic_string<T>& string, S16& value); -	static BOOL	convertToU16(const std::basic_string<T>& string, U16& value); -	static BOOL	convertToU32(const std::basic_string<T>& string, U32& value); -	static BOOL	convertToS32(const std::basic_string<T>& string, S32& value); -	static BOOL	convertToF32(const std::basic_string<T>& string, F32& value); -	static BOOL	convertToF64(const std::basic_string<T>& string, F64& value); +	static BOOL	convertToBOOL(const string_type& string, BOOL& value); +	static BOOL	convertToU8(const string_type& string, U8& value); +	static BOOL	convertToS8(const string_type& string, S8& value); +	static BOOL	convertToS16(const string_type& string, S16& value); +	static BOOL	convertToU16(const string_type& string, U16& value); +	static BOOL	convertToU32(const string_type& string, U32& value); +	static BOOL	convertToS32(const string_type& string, S32& value); +	static BOOL	convertToF32(const string_type& string, F32& value); +	static BOOL	convertToF64(const string_type& string, F64& value);  	/////////////////////////////////////////////////////////////////////////////////////////  	// Utility functions for working with char*'s and strings @@ -327,24 +378,24 @@ public:  	// Like strcmp but also handles empty strings. Uses  	// current locale.  	static S32		compareStrings(const T* lhs, const T* rhs); -	static S32		compareStrings(const std::basic_string<T>& lhs, const std::basic_string<T>& rhs); +	static S32		compareStrings(const string_type& lhs, const string_type& rhs);  	// case insensitive version of above. Uses current locale on  	// Win32, and falls back to a non-locale aware comparison on  	// Linux.  	static S32		compareInsensitive(const T* lhs, const T* rhs); -	static S32		compareInsensitive(const std::basic_string<T>& lhs, const std::basic_string<T>& rhs); +	static S32		compareInsensitive(const string_type& lhs, const string_type& rhs);  	// Case sensitive comparison with good handling of numbers.  Does not use current locale.  	// a.k.a. strdictcmp() -	static S32		compareDict(const std::basic_string<T>& a, const std::basic_string<T>& b); +	static S32		compareDict(const string_type& a, const string_type& b);  	// Case *in*sensitive comparison with good handling of numbers.  Does not use current locale.  	// a.k.a. strdictcmp() -	static S32		compareDictInsensitive(const std::basic_string<T>& a, const std::basic_string<T>& b); +	static S32		compareDictInsensitive(const string_type& a, const string_type& b);  	// Puts compareDict() in a form appropriate for LL container classes to use for sorting. -	static BOOL		precedesDict( const std::basic_string<T>& a, const std::basic_string<T>& b ); +	static BOOL		precedesDict( const string_type& a, const string_type& b );  	// A replacement for strncpy.  	// If the dst buffer is dst_size bytes long or more, ensures that dst is null terminated and holds @@ -352,7 +403,7 @@ public:  	static void		copy(T* dst, const T* src, size_type dst_size);  	// Copies src into dst at a given offset.   -	static void		copyInto(std::basic_string<T>& dst, const std::basic_string<T>& src, size_type offset); +	static void		copyInto(string_type& dst, const string_type& src, size_type offset);  	static bool		isPartOfWord(T c) { return (c == (T)'_') || LLStringOps::isAlnum(c); } @@ -362,7 +413,7 @@ public:  #endif  private: -	LL_COMMON_API static size_type getSubstitution(const std::basic_string<T>& instr, size_type& start, std::vector<std::basic_string<T> >& tokens); +	LL_COMMON_API static size_type getSubstitution(const string_type& instr, size_type& start, std::vector<string_type >& tokens);  };  template<class T> const std::basic_string<T> LLStringUtilBase<T>::null; @@ -636,10 +687,325 @@ namespace LLStringFn  ////////////////////////////////////////////////////////////  // NOTE: LLStringUtil::format, getTokens, and support functions moved to llstring.cpp.  // There is no LLWStringUtil::format implementation currently. -// Calling thse for anything other than LLStringUtil will produce link errors. +// Calling these for anything other than LLStringUtil will produce link errors.  //////////////////////////////////////////////////////////// +// static +template <class T> +std::vector<typename LLStringUtilBase<T>::string_type> +LLStringUtilBase<T>::getTokens(const string_type& instr, const string_type& delims) +{ +	std::vector<string_type> tokens; +	getTokens(instr, tokens, delims); +	return tokens; +} + +// static +template <class T> +std::vector<typename LLStringUtilBase<T>::string_type> +LLStringUtilBase<T>::getTokens(const string_type& instr, +							   const string_type& drop_delims, +							   const string_type& keep_delims, +							   const string_type& quotes) +{ +	std::vector<string_type> tokens; +	getTokens(instr, tokens, drop_delims, keep_delims, quotes); +	return tokens; +} + +// static +template <class T> +std::vector<typename LLStringUtilBase<T>::string_type> +LLStringUtilBase<T>::getTokens(const string_type& instr, +							   const string_type& drop_delims, +							   const string_type& keep_delims, +							   const string_type& quotes, +							   const string_type& escapes) +{ +	std::vector<string_type> tokens; +	getTokens(instr, tokens, drop_delims, keep_delims, quotes, escapes); +	return tokens; +} + +namespace LLStringUtilBaseImpl +{ + +/** + * Input string scanner helper for getTokens(), or really any other + * character-parsing routine that may have to deal with escape characters. + * This implementation defines the concept (also an interface, should you + * choose to implement the concept by subclassing) and provides trivial + * implementations for a string @em without escape processing. + */ +template <class T> +struct InString +{ +	typedef std::basic_string<T> string_type; +	typedef typename string_type::const_iterator const_iterator; + +	InString(const_iterator b, const_iterator e): +		mIter(b), +		mEnd(e) +	{} +	virtual ~InString() {} + +	bool done() const { return mIter == mEnd; } +	/// Is the current character (*mIter) escaped? This implementation can +	/// answer trivially because it doesn't support escapes. +	virtual bool escaped() const { return false; } +	/// Obtain the current character and advance @c mIter. +	virtual T next() { return *mIter++; } +	/// Does the current character match specified character? +	virtual bool is(T ch) const { return (! done()) && *mIter == ch; } +	/// Is the current character any one of the specified characters? +	virtual bool oneof(const string_type& delims) const +	{ +		return (! done()) && LLStringUtilBase<T>::contains(delims, *mIter); +	} + +	/** +	 * Scan forward from @from until either @a delim or end. This is primarily +	 * useful for processing quoted substrings. +	 * +	 * If we do see @a delim, append everything from @from until (excluding) +	 * @a delim to @a into, advance @c mIter to skip @a delim, and return @c +	 * true. +	 * +	 * If we do not see @a delim, do not alter @a into or @c mIter and return +	 * @c false. Do not pass GO, do not collect $200. +	 * +	 * @note The @c false case described above implements normal getTokens() +	 * treatment of an unmatched open quote: treat the quote character as if +	 * escaped, that is, simply collect it as part of the current token. Other +	 * plausible behaviors directly affect the way getTokens() deals with an +	 * unmatched quote: e.g. throwing an exception to treat it as an error, or +	 * assuming a close quote beyond end of string (in which case return @c +	 * true). +	 */ +	virtual bool collect_until(string_type& into, const_iterator from, T delim) +	{ +		const_iterator found = std::find(from, mEnd, delim); +		// If we didn't find delim, change nothing, just tell caller. +		if (found == mEnd) +			return false; +		// Found delim! Append everything between from and found. +		into.append(from, found); +		// advance past delim in input +		mIter = found + 1; +		return true; +	} + +	const_iterator mIter, mEnd; +}; + +/// InString subclass that handles escape characters +template <class T> +class InEscString: public InString<T> +{ +public: +	typedef InString<T> super; +	typedef typename super::string_type string_type; +	typedef typename super::const_iterator const_iterator; +	using super::done; +	using super::mIter; +	using super::mEnd; + +	InEscString(const_iterator b, const_iterator e, const string_type& escapes): +		super(b, e), +		mEscapes(escapes) +	{ +		// Even though we've already initialized 'mIter' via our base-class +		// constructor, set it again to check for initial escape char. +		setiter(b); +	} + +	/// This implementation uses the answer cached by setiter(). +	virtual bool escaped() const { return mIsEsc; } +	virtual T next() +	{ +		// If we're looking at the escape character of an escape sequence, +		// skip that character. This is the one time we can modify 'mIter' +		// without using setiter: for this one case we DO NOT CARE if the +		// escaped character is itself an escape. +		if (mIsEsc) +			++mIter; +		// If we were looking at an escape character, this is the escaped +		// character; otherwise it's just the next character. +		T result(*mIter); +		// Advance mIter, checking for escape sequence. +		setiter(mIter + 1); +		return result; +	} + +	virtual bool is(T ch) const +	{ +		// Like base-class is(), except that an escaped character matches +		// nothing. +		return (! done()) && (! mIsEsc) && *mIter == ch; +	} + +	virtual bool oneof(const string_type& delims) const +	{ +		// Like base-class oneof(), except that an escaped character matches +		// nothing. +		return (! done()) && (! mIsEsc) && LLStringUtilBase<T>::contains(delims, *mIter); +	} + +	virtual bool collect_until(string_type& into, const_iterator from, T delim) +	{ +		// Deal with escapes in the characters we collect; that is, an escaped +		// character must become just that character without the preceding +		// escape. Collect characters in a separate string rather than +		// directly appending to 'into' in case we do not find delim, in which +		// case we're supposed to leave 'into' unmodified. +		string_type collected; +		// For scanning purposes, we're going to work directly with 'mIter'. +		// Save its current value in case we fail to see delim. +		const_iterator save_iter(mIter); +		// Okay, set 'mIter', checking for escape. +		setiter(from); +		while (! done()) +		{ +			// If we see an unescaped delim, stop and report success. +			if ((! mIsEsc) && *mIter == delim) +			{ +				// Append collected chars to 'into'. +				into.append(collected); +				// Don't forget to advance 'mIter' past delim. +				setiter(mIter + 1); +				return true; +			} +			// We're not at end, and either we're not looking at delim or it's +			// escaped. Collect this character and keep going. +			collected.push_back(next()); +		} +		// Here we hit 'mEnd' without ever seeing delim. Restore mIter and tell +		// caller. +		setiter(save_iter); +		return false; +	} + +private: +	void setiter(const_iterator i) +	{ +		mIter = i; + +		// Every time we change 'mIter', set 'mIsEsc' to be able to repetitively +		// answer escaped() without having to rescan 'mEscapes'. mIsEsc caches +		// contains(mEscapes, *mIter). + +		// We're looking at an escaped char if we're not already at end (that +		// is, *mIter is even meaningful); if *mIter is in fact one of the +		// specified escape characters; and if there's one more character +		// following it. That is, if an escape character is the very last +		// character of the input string, it loses its special meaning. +		mIsEsc = (! done()) && +				LLStringUtilBase<T>::contains(mEscapes, *mIter) && +				(mIter+1) != mEnd; +	} + +	const string_type mEscapes; +	bool mIsEsc; +}; + +/// getTokens() implementation based on InString concept +template <typename INSTRING, typename string_type> +void getTokens(INSTRING& instr, std::vector<string_type>& tokens, +			   const string_type& drop_delims, const string_type& keep_delims, +			   const string_type& quotes) +{ +	// There are times when we want to match either drop_delims or +	// keep_delims. Concatenate them up front to speed things up. +	string_type all_delims(drop_delims + keep_delims); +	// no tokens yet +	tokens.clear(); + +	// try for another token +	while (! instr.done()) +	{ +		// scan past any drop_delims +		while (instr.oneof(drop_delims)) +		{ +			// skip this drop_delim +			instr.next(); +			// but if that was the end of the string, done +			if (instr.done()) +				return; +		} +		// found the start of another token: make a slot for it +		tokens.push_back(string_type()); +		if (instr.oneof(keep_delims)) +		{ +			// *iter is a keep_delim, a token of exactly 1 character. Append +			// that character to the new token and proceed. +			tokens.back().push_back(instr.next()); +			continue; +		} +		// Here we have a non-delimiter token, which might consist of a mix of +		// quoted and unquoted parts. Use bash rules for quoting: you can +		// embed a quoted substring in the midst of an unquoted token (e.g. +		// ~/"sub dir"/myfile.txt); you can ram two quoted substrings together +		// to make a single token (e.g. 'He said, "'"Don't."'"'). We diverge +		// from bash in that bash considers an unmatched quote an error. Our +		// param signature doesn't allow for errors, so just pretend it's not +		// a quote and embed it. +		// At this level, keep scanning until we hit the next delimiter of +		// either type (drop_delims or keep_delims). +		while (! instr.oneof(all_delims)) +		{ +			// If we're looking at an open quote, search forward for +			// a close quote, collecting characters along the way. +			if (instr.oneof(quotes) && +				instr.collect_until(tokens.back(), instr.mIter+1, *instr.mIter)) +			{ +				// collect_until is cleverly designed to do exactly what we +				// need here. No further action needed if it returns true. +			} +			else +			{ +				// Either *iter isn't a quote, or there's no matching close +				// quote: in other words, just an ordinary char. Append it to +				// current token. +				tokens.back().push_back(instr.next()); +			} +			// having scanned that segment of this token, if we've reached the +			// end of the string, we're done +			if (instr.done()) +				return; +		} +	} +} + +} // namespace LLStringUtilBaseImpl + +// static +template <class T> +void LLStringUtilBase<T>::getTokens(const string_type& string, std::vector<string_type>& tokens, +									const string_type& drop_delims, const string_type& keep_delims, +									const string_type& quotes) +{ +	// Because this overload doesn't support escapes, use simple InString to +	// manage input range. +	LLStringUtilBaseImpl::InString<T> instring(string.begin(), string.end()); +	LLStringUtilBaseImpl::getTokens(instring, tokens, drop_delims, keep_delims, quotes); +} + +// static +template <class T> +void LLStringUtilBase<T>::getTokens(const string_type& string, std::vector<string_type>& tokens, +									const string_type& drop_delims, const string_type& keep_delims, +									const string_type& quotes, const string_type& escapes) +{ +	// This overload must deal with escapes. Delegate that to InEscString +	// (unless there ARE no escapes). +	boost::scoped_ptr< LLStringUtilBaseImpl::InString<T> > instrp; +	if (escapes.empty()) +		instrp.reset(new LLStringUtilBaseImpl::InString<T>(string.begin(), string.end())); +	else +		instrp.reset(new LLStringUtilBaseImpl::InEscString<T>(string.begin(), string.end(), escapes)); +	LLStringUtilBaseImpl::getTokens(*instrp, tokens, drop_delims, keep_delims, quotes); +}  // static  template<class T>  @@ -669,7 +1035,7 @@ S32 LLStringUtilBase<T>::compareStrings(const T* lhs, const T* rhs)  //static   template<class T>  -S32 LLStringUtilBase<T>::compareStrings(const std::basic_string<T>& lhs, const std::basic_string<T>& rhs) +S32 LLStringUtilBase<T>::compareStrings(const string_type& lhs, const string_type& rhs)  {  	return LLStringOps::collate(lhs.c_str(), rhs.c_str());  } @@ -695,8 +1061,8 @@ S32 LLStringUtilBase<T>::compareInsensitive(const T* lhs, const T* rhs )  	}  	else  	{ -		std::basic_string<T> lhs_string(lhs); -		std::basic_string<T> rhs_string(rhs); +		string_type lhs_string(lhs); +		string_type rhs_string(rhs);  		LLStringUtilBase<T>::toUpper(lhs_string);  		LLStringUtilBase<T>::toUpper(rhs_string);  		result = LLStringOps::collate(lhs_string.c_str(), rhs_string.c_str()); @@ -706,10 +1072,10 @@ S32 LLStringUtilBase<T>::compareInsensitive(const T* lhs, const T* rhs )  //static   template<class T>  -S32 LLStringUtilBase<T>::compareInsensitive(const std::basic_string<T>& lhs, const std::basic_string<T>& rhs) +S32 LLStringUtilBase<T>::compareInsensitive(const string_type& lhs, const string_type& rhs)  { -	std::basic_string<T> lhs_string(lhs); -	std::basic_string<T> rhs_string(rhs); +	string_type lhs_string(lhs); +	string_type rhs_string(rhs);  	LLStringUtilBase<T>::toUpper(lhs_string);  	LLStringUtilBase<T>::toUpper(rhs_string);  	return LLStringOps::collate(lhs_string.c_str(), rhs_string.c_str()); @@ -720,7 +1086,7 @@ S32 LLStringUtilBase<T>::compareInsensitive(const std::basic_string<T>& lhs, con  //static   template<class T> -S32 LLStringUtilBase<T>::compareDict(const std::basic_string<T>& astr, const std::basic_string<T>& bstr) +S32 LLStringUtilBase<T>::compareDict(const string_type& astr, const string_type& bstr)  {  	const T* a = astr.c_str();  	const T* b = bstr.c_str(); @@ -761,7 +1127,7 @@ S32 LLStringUtilBase<T>::compareDict(const std::basic_string<T>& astr, const std  // static  template<class T> -S32 LLStringUtilBase<T>::compareDictInsensitive(const std::basic_string<T>& astr, const std::basic_string<T>& bstr) +S32 LLStringUtilBase<T>::compareDictInsensitive(const string_type& astr, const string_type& bstr)  {  	const T* a = astr.c_str();  	const T* b = bstr.c_str(); @@ -796,7 +1162,7 @@ S32 LLStringUtilBase<T>::compareDictInsensitive(const std::basic_string<T>& astr  // Puts compareDict() in a form appropriate for LL container classes to use for sorting.  // static   template<class T>  -BOOL LLStringUtilBase<T>::precedesDict( const std::basic_string<T>& a, const std::basic_string<T>& b ) +BOOL LLStringUtilBase<T>::precedesDict( const string_type& a, const string_type& b )  {  	if( a.size() && b.size() )  	{ @@ -810,7 +1176,7 @@ BOOL LLStringUtilBase<T>::precedesDict( const std::basic_string<T>& a, const std  //static  template<class T>  -void LLStringUtilBase<T>::toUpper(std::basic_string<T>& string)	 +void LLStringUtilBase<T>::toUpper(string_type& string)	  {   	if( !string.empty() )  	{  @@ -824,7 +1190,7 @@ void LLStringUtilBase<T>::toUpper(std::basic_string<T>& string)  //static  template<class T>  -void LLStringUtilBase<T>::toLower(std::basic_string<T>& string) +void LLStringUtilBase<T>::toLower(string_type& string)  {   	if( !string.empty() )  	{  @@ -838,7 +1204,7 @@ void LLStringUtilBase<T>::toLower(std::basic_string<T>& string)  //static  template<class T>  -void LLStringUtilBase<T>::trimHead(std::basic_string<T>& string) +void LLStringUtilBase<T>::trimHead(string_type& string)  {			  	if( !string.empty() )  	{ @@ -853,7 +1219,7 @@ void LLStringUtilBase<T>::trimHead(std::basic_string<T>& string)  //static  template<class T>  -void LLStringUtilBase<T>::trimTail(std::basic_string<T>& string) +void LLStringUtilBase<T>::trimTail(string_type& string)  {			  	if( string.size() )  	{ @@ -872,7 +1238,7 @@ void LLStringUtilBase<T>::trimTail(std::basic_string<T>& string)  // Replace line feeds with carriage return-line feed pairs.  //static  template<class T> -void LLStringUtilBase<T>::addCRLF(std::basic_string<T>& string) +void LLStringUtilBase<T>::addCRLF(string_type& string)  {  	const T LF = 10;  	const T CR = 13; @@ -914,7 +1280,7 @@ void LLStringUtilBase<T>::addCRLF(std::basic_string<T>& string)  // Remove all carriage returns  //static  template<class T>  -void LLStringUtilBase<T>::removeCRLF(std::basic_string<T>& string) +void LLStringUtilBase<T>::removeCRLF(string_type& string)  {  	const T CR = 13; @@ -935,10 +1301,10 @@ void LLStringUtilBase<T>::removeCRLF(std::basic_string<T>& string)  //static  template<class T>  -void LLStringUtilBase<T>::replaceChar( std::basic_string<T>& string, T target, T replacement ) +void LLStringUtilBase<T>::replaceChar( string_type& string, T target, T replacement )  {  	size_type found_pos = 0; -	while( (found_pos = string.find(target, found_pos)) != std::basic_string<T>::npos )  +	while( (found_pos = string.find(target, found_pos)) != string_type::npos )   	{  		string[found_pos] = replacement;  		found_pos++; // avoid infinite defeat if target == replacement @@ -947,10 +1313,10 @@ void LLStringUtilBase<T>::replaceChar( std::basic_string<T>& string, T target, T  //static  template<class T>  -void LLStringUtilBase<T>::replaceString( std::basic_string<T>& string, std::basic_string<T> target, std::basic_string<T> replacement ) +void LLStringUtilBase<T>::replaceString( string_type& string, string_type target, string_type replacement )  {  	size_type found_pos = 0; -	while( (found_pos = string.find(target, found_pos)) != std::basic_string<T>::npos ) +	while( (found_pos = string.find(target, found_pos)) != string_type::npos )  	{  		string.replace( found_pos, target.length(), replacement );  		found_pos += replacement.length(); // avoid infinite defeat if replacement contains target @@ -959,7 +1325,7 @@ void LLStringUtilBase<T>::replaceString( std::basic_string<T>& string, std::basi  //static  template<class T>  -void LLStringUtilBase<T>::replaceNonstandardASCII( std::basic_string<T>& string, T replacement ) +void LLStringUtilBase<T>::replaceNonstandardASCII( string_type& string, T replacement )  {  	const char LF = 10;  	const S8 MIN = 32; @@ -979,12 +1345,12 @@ void LLStringUtilBase<T>::replaceNonstandardASCII( std::basic_string<T>& string,  //static  template<class T>  -void LLStringUtilBase<T>::replaceTabsWithSpaces( std::basic_string<T>& str, size_type spaces_per_tab ) +void LLStringUtilBase<T>::replaceTabsWithSpaces( string_type& str, size_type spaces_per_tab )  {  	const T TAB = '\t';  	const T SPACE = ' '; -	std::basic_string<T> out_str; +	string_type out_str;  	// Replace tabs with spaces  	for (size_type i = 0; i < str.length(); i++)  	{ @@ -1003,7 +1369,7 @@ void LLStringUtilBase<T>::replaceTabsWithSpaces( std::basic_string<T>& str, size  //static  template<class T>  -BOOL LLStringUtilBase<T>::containsNonprintable(const std::basic_string<T>& string) +BOOL LLStringUtilBase<T>::containsNonprintable(const string_type& string)  {  	const char MIN = 32;  	BOOL rv = FALSE; @@ -1020,7 +1386,7 @@ BOOL LLStringUtilBase<T>::containsNonprintable(const std::basic_string<T>& strin  //static  template<class T>  -void LLStringUtilBase<T>::stripNonprintable(std::basic_string<T>& string) +void LLStringUtilBase<T>::stripNonprintable(string_type& string)  {  	const char MIN = 32;  	size_type j = 0; @@ -1051,8 +1417,43 @@ void LLStringUtilBase<T>::stripNonprintable(std::basic_string<T>& string)  	delete []c_string;  } +template<class T> +std::basic_string<T> LLStringUtilBase<T>::quote(const string_type& str, +												const string_type& triggers, +												const string_type& escape) +{ +	size_type len(str.length()); +	// If the string is already quoted, assume user knows what s/he's doing. +	if (len >= 2 && str[0] == '"' && str[len-1] == '"') +	{ +		return str; +	} + +	// Not already quoted: do we need to? triggers.empty() is a special case +	// meaning "always quote." +	if ((! triggers.empty()) && str.find_first_of(triggers) == string_type::npos) +	{ +		// no trigger characters, don't bother quoting +		return str; +	} + +	// For whatever reason, we must quote this string. +	string_type result; +	result.push_back('"'); +	for (typename string_type::const_iterator ci(str.begin()), cend(str.end()); ci != cend; ++ci) +	{ +		if (*ci == '"') +		{ +			result.append(escape); +		} +		result.push_back(*ci); +	} +	result.push_back('"'); +	return result; +} +  template<class T>  -void LLStringUtilBase<T>::_makeASCII(std::basic_string<T>& string) +void LLStringUtilBase<T>::_makeASCII(string_type& string)  {  	// Replace non-ASCII chars with LL_UNKNOWN_CHAR  	for (size_type i = 0; i < string.length(); i++) @@ -1082,7 +1483,7 @@ void LLStringUtilBase<T>::copy( T* dst, const T* src, size_type dst_size )  // static  template<class T>  -void LLStringUtilBase<T>::copyInto(std::basic_string<T>& dst, const std::basic_string<T>& src, size_type offset) +void LLStringUtilBase<T>::copyInto(string_type& dst, const string_type& src, size_type offset)  {  	if ( offset == dst.length() )  	{ @@ -1092,7 +1493,7 @@ void LLStringUtilBase<T>::copyInto(std::basic_string<T>& dst, const std::basic_s  	}  	else  	{ -		std::basic_string<T> tail = dst.substr(offset); +		string_type tail = dst.substr(offset);  		dst = dst.substr(0, offset);  		dst += src; @@ -1103,7 +1504,7 @@ void LLStringUtilBase<T>::copyInto(std::basic_string<T>& dst, const std::basic_s  // True if this is the head of s.  //static  template<class T>  -BOOL LLStringUtilBase<T>::isHead( const std::basic_string<T>& string, const T* s )  +BOOL LLStringUtilBase<T>::isHead( const string_type& string, const T* s )   {   	if( string.empty() )  	{ @@ -1119,8 +1520,8 @@ BOOL LLStringUtilBase<T>::isHead( const std::basic_string<T>& string, const T* s  // static  template<class T>   bool LLStringUtilBase<T>::startsWith( -	const std::basic_string<T>& string, -	const std::basic_string<T>& substr) +	const string_type& string, +	const string_type& substr)  {  	if(string.empty() || (substr.empty())) return false;  	if(0 == string.find(substr)) return true; @@ -1130,8 +1531,8 @@ bool LLStringUtilBase<T>::startsWith(  // static  template<class T>   bool LLStringUtilBase<T>::endsWith( -	const std::basic_string<T>& string, -	const std::basic_string<T>& substr) +	const string_type& string, +	const string_type& substr)  {  	if(string.empty() || (substr.empty())) return false;  	std::string::size_type idx = string.rfind(substr); @@ -1141,14 +1542,14 @@ bool LLStringUtilBase<T>::endsWith(  template<class T>  -BOOL LLStringUtilBase<T>::convertToBOOL(const std::basic_string<T>& string, BOOL& value) +BOOL LLStringUtilBase<T>::convertToBOOL(const string_type& string, BOOL& value)  {  	if( string.empty() )  	{  		return FALSE;  	} -	std::basic_string<T> temp( string ); +	string_type temp( string );  	trim(temp);  	if(   		(temp == "1") ||  @@ -1178,7 +1579,7 @@ BOOL LLStringUtilBase<T>::convertToBOOL(const std::basic_string<T>& string, BOOL  }  template<class T>  -BOOL LLStringUtilBase<T>::convertToU8(const std::basic_string<T>& string, U8& value)  +BOOL LLStringUtilBase<T>::convertToU8(const string_type& string, U8& value)   {  	S32 value32 = 0;  	BOOL success = convertToS32(string, value32); @@ -1191,7 +1592,7 @@ BOOL LLStringUtilBase<T>::convertToU8(const std::basic_string<T>& string, U8& va  }  template<class T>  -BOOL LLStringUtilBase<T>::convertToS8(const std::basic_string<T>& string, S8& value)  +BOOL LLStringUtilBase<T>::convertToS8(const string_type& string, S8& value)   {  	S32 value32 = 0;  	BOOL success = convertToS32(string, value32); @@ -1204,7 +1605,7 @@ BOOL LLStringUtilBase<T>::convertToS8(const std::basic_string<T>& string, S8& va  }  template<class T>  -BOOL LLStringUtilBase<T>::convertToS16(const std::basic_string<T>& string, S16& value)  +BOOL LLStringUtilBase<T>::convertToS16(const string_type& string, S16& value)   {  	S32 value32 = 0;  	BOOL success = convertToS32(string, value32); @@ -1217,7 +1618,7 @@ BOOL LLStringUtilBase<T>::convertToS16(const std::basic_string<T>& string, S16&  }  template<class T>  -BOOL LLStringUtilBase<T>::convertToU16(const std::basic_string<T>& string, U16& value)  +BOOL LLStringUtilBase<T>::convertToU16(const string_type& string, U16& value)   {  	S32 value32 = 0;  	BOOL success = convertToS32(string, value32); @@ -1230,17 +1631,17 @@ BOOL LLStringUtilBase<T>::convertToU16(const std::basic_string<T>& string, U16&  }  template<class T>  -BOOL LLStringUtilBase<T>::convertToU32(const std::basic_string<T>& string, U32& value)  +BOOL LLStringUtilBase<T>::convertToU32(const string_type& string, U32& value)   {  	if( string.empty() )  	{  		return FALSE;  	} -	std::basic_string<T> temp( string ); +	string_type temp( string );  	trim(temp);  	U32 v; -	std::basic_istringstream<T> i_stream((std::basic_string<T>)temp); +	std::basic_istringstream<T> i_stream((string_type)temp);  	if(i_stream >> v)  	{  		value = v; @@ -1250,17 +1651,17 @@ BOOL LLStringUtilBase<T>::convertToU32(const std::basic_string<T>& string, U32&  }  template<class T>  -BOOL LLStringUtilBase<T>::convertToS32(const std::basic_string<T>& string, S32& value)  +BOOL LLStringUtilBase<T>::convertToS32(const string_type& string, S32& value)   {  	if( string.empty() )  	{  		return FALSE;  	} -	std::basic_string<T> temp( string ); +	string_type temp( string );  	trim(temp);  	S32 v; -	std::basic_istringstream<T> i_stream((std::basic_string<T>)temp); +	std::basic_istringstream<T> i_stream((string_type)temp);  	if(i_stream >> v)  	{  		//TODO: figure out overflow and underflow reporting here @@ -1277,7 +1678,7 @@ BOOL LLStringUtilBase<T>::convertToS32(const std::basic_string<T>& string, S32&  }  template<class T>  -BOOL LLStringUtilBase<T>::convertToF32(const std::basic_string<T>& string, F32& value)  +BOOL LLStringUtilBase<T>::convertToF32(const string_type& string, F32& value)   {  	F64 value64 = 0.0;  	BOOL success = convertToF64(string, value64); @@ -1290,17 +1691,17 @@ BOOL LLStringUtilBase<T>::convertToF32(const std::basic_string<T>& string, F32&  }  template<class T>  -BOOL LLStringUtilBase<T>::convertToF64(const std::basic_string<T>& string, F64& value) +BOOL LLStringUtilBase<T>::convertToF64(const string_type& string, F64& value)  {  	if( string.empty() )  	{  		return FALSE;  	} -	std::basic_string<T> temp( string ); +	string_type temp( string );  	trim(temp);  	F64 v; -	std::basic_istringstream<T> i_stream((std::basic_string<T>)temp); +	std::basic_istringstream<T> i_stream((string_type)temp);  	if(i_stream >> v)  	{  		//TODO: figure out overflow and underflow reporting here @@ -1317,7 +1718,7 @@ BOOL LLStringUtilBase<T>::convertToF64(const std::basic_string<T>& string, F64&  }  template<class T>  -void LLStringUtilBase<T>::truncate(std::basic_string<T>& string, size_type count) +void LLStringUtilBase<T>::truncate(string_type& string, size_type count)  {  	size_type cur_size = string.size();  	string.resize(count < cur_size ? count : cur_size); diff --git a/indra/llcommon/llsys.cpp b/indra/llcommon/llsys.cpp index 19075afa68..6073bcd0a6 100644 --- a/indra/llcommon/llsys.cpp +++ b/indra/llcommon/llsys.cpp @@ -1364,11 +1364,21 @@ BOOL gzip_file(const std::string& srcfile, const std::string& dstfile)  	src = LLFile::fopen(srcfile, "rb");		/* Flawfinder: ignore */  	if (! src) goto err; -	do +	while ((bytes = (S32)fread(buffer, sizeof(U8), COMPRESS_BUFFER_SIZE, src)) > 0)  	{ -		bytes = (S32)fread(buffer, sizeof(U8), COMPRESS_BUFFER_SIZE,src); -		gzwrite(dst, buffer, bytes); -	} while(feof(src) == 0); +		if (gzwrite(dst, buffer, bytes) <= 0) +		{ +			llwarns << "gzwrite failed: " << gzerror(dst, NULL) << llendl; +			goto err; +		} +	} + +	if (ferror(src)) +	{ +		llwarns << "Error reading " << srcfile << llendl; +		goto err; +	} +  	gzclose(dst);  	dst = NULL;  #if LL_WINDOWS diff --git a/indra/llcommon/llthread.cpp b/indra/llcommon/llthread.cpp index 4063cc730b..a6ad6b125c 100644 --- a/indra/llcommon/llthread.cpp +++ b/indra/llcommon/llthread.cpp @@ -337,11 +337,7 @@ LLMutex::~LLMutex()  void LLMutex::lock()  { -#if LL_DARWIN -	if (mLockingThread == LLThread::currentID()) -#else -	if (mLockingThread == sThreadID) -#endif +	if(isSelfLocked())  	{ //redundant lock  		mCount++;  		return; @@ -398,6 +394,15 @@ bool LLMutex::isLocked()  	}  } +bool LLMutex::isSelfLocked() +{ +#if LL_DARWIN +	return mLockingThread == LLThread::currentID(); +#else +	return mLockingThread == sThreadID; +#endif +} +  U32 LLMutex::lockingThread() const  {  	return mLockingThread; diff --git a/indra/llcommon/llthread.h b/indra/llcommon/llthread.h index 40291a2569..b52e70ab2e 100644 --- a/indra/llcommon/llthread.h +++ b/indra/llcommon/llthread.h @@ -151,6 +151,7 @@ public:  	void lock();		// blocks  	void unlock();  	bool isLocked(); 	// non-blocking, but does do a lock/unlock so not free +	bool isSelfLocked(); //return true if locked in a same thread  	U32 lockingThread() const; //get ID of locking thread  protected: @@ -187,11 +188,14 @@ public:  	LLMutexLock(LLMutex* mutex)  	{  		mMutex = mutex; -		mMutex->lock(); +		 +		if(mMutex) +			mMutex->lock();  	}  	~LLMutexLock()  	{ -		mMutex->unlock(); +		if(mMutex) +			mMutex->unlock();  	}  private:  	LLMutex* mMutex; diff --git a/indra/llcommon/llversionviewer.h b/indra/llcommon/llversionviewer.h index ec378761c2..a869c74189 100644 --- a/indra/llcommon/llversionviewer.h +++ b/indra/llcommon/llversionviewer.h @@ -28,8 +28,8 @@  #define LL_LLVERSIONVIEWER_H  const S32 LL_VERSION_MAJOR = 3; -const S32 LL_VERSION_MINOR = 2; -const S32 LL_VERSION_PATCH = 6; +const S32 LL_VERSION_MINOR = 3; +const S32 LL_VERSION_PATCH = 0;  const S32 LL_VERSION_BUILD = 0;  const char * const LL_CHANNEL = "Second Life Developer"; diff --git a/indra/llcommon/llworkerthread.cpp b/indra/llcommon/llworkerthread.cpp index 4988bdf570..3d05a30ac2 100644 --- a/indra/llcommon/llworkerthread.cpp +++ b/indra/llcommon/llworkerthread.cpp @@ -81,7 +81,7 @@ void LLWorkerThread::clearDeleteList()  }  // virtual -S32 LLWorkerThread::update(U32 max_time_ms) +S32 LLWorkerThread::update(F32 max_time_ms)  {  	S32 res = LLQueuedThread::update(max_time_ms);  	// Delete scheduled workers diff --git a/indra/llcommon/llworkerthread.h b/indra/llcommon/llworkerthread.h index 78a4781d15..be46394d6e 100644 --- a/indra/llcommon/llworkerthread.h +++ b/indra/llcommon/llworkerthread.h @@ -86,7 +86,7 @@ public:  	LLWorkerThread(const std::string& name, bool threaded = true, bool should_pause = false);  	~LLWorkerThread(); -	/*virtual*/ S32 update(U32 max_time_ms); +	/*virtual*/ S32 update(F32 max_time_ms);  	handle_t addWorkRequest(LLWorkerClass* workerclass, S32 param, U32 priority = PRIORITY_NORMAL); diff --git a/indra/llcommon/tests/StringVec.h b/indra/llcommon/tests/StringVec.h new file mode 100644 index 0000000000..a380b00a05 --- /dev/null +++ b/indra/llcommon/tests/StringVec.h @@ -0,0 +1,37 @@ +/** + * @file   StringVec.h + * @author Nat Goodspeed + * @date   2012-02-24 + * @brief  Extend TUT ensure_equals() to handle std::vector<std::string> + *  + * $LicenseInfo:firstyear=2012&license=viewerlgpl$ + * Copyright (c) 2012, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_STRINGVEC_H) +#define LL_STRINGVEC_H + +#include <vector> +#include <string> +#include <iostream> + +typedef std::vector<std::string> StringVec; + +std::ostream& operator<<(std::ostream& out, const StringVec& strings) +{ +    out << '('; +    StringVec::const_iterator begin(strings.begin()), end(strings.end()); +    if (begin != end) +    { +        out << '"' << *begin << '"'; +        while (++begin != end) +        { +            out << ", \"" << *begin << '"'; +        } +    } +    out << ')'; +    return out; +} + +#endif /* ! defined(LL_STRINGVEC_H) */ diff --git a/indra/llcommon/tests/listener.h b/indra/llcommon/tests/listener.h index dcdb2412be..9c5c18a150 100644 --- a/indra/llcommon/tests/listener.h +++ b/indra/llcommon/tests/listener.h @@ -30,6 +30,8 @@  #define LL_LISTENER_H  #include "llsd.h" +#include "llevents.h" +#include "tests/StringVec.h"  #include <iostream>  /***************************************************************************** @@ -133,24 +135,7 @@ struct Collect          return false;      }      void clear() { result.clear(); } -    typedef std::vector<std::string> StringList; -    StringList result; +    StringVec result;  }; -std::ostream& operator<<(std::ostream& out, const Collect::StringList& strings) -{ -    out << '('; -    Collect::StringList::const_iterator begin(strings.begin()), end(strings.end()); -    if (begin != end) -    { -        out << '"' << *begin << '"'; -        while (++begin != end) -        { -            out << ", \"" << *begin << '"'; -        } -    } -    out << ')'; -    return out; -} -  #endif /* ! defined(LL_LISTENER_H) */ diff --git a/indra/llcommon/tests/llinstancetracker_test.cpp b/indra/llcommon/tests/llinstancetracker_test.cpp index b34d1c5fd3..454695ff9f 100644 --- a/indra/llcommon/tests/llinstancetracker_test.cpp +++ b/indra/llcommon/tests/llinstancetracker_test.cpp @@ -35,6 +35,7 @@  #include <vector>  #include <set>  #include <algorithm>                // std::sort() +#include <stdexcept>  // std headers  // external library headers  #include <boost/scoped_ptr.hpp> @@ -42,6 +43,11 @@  #include "../test/lltut.h"  #include "wrapllerrs.h" +struct Badness: public std::runtime_error +{ +    Badness(const std::string& what): std::runtime_error(what) {} +}; +  struct Keyed: public LLInstanceTracker<Keyed, std::string>  {      Keyed(const std::string& name): @@ -53,6 +59,17 @@ struct Keyed: public LLInstanceTracker<Keyed, std::string>  struct Unkeyed: public LLInstanceTracker<Unkeyed>  { +    Unkeyed(const std::string& thrw="") +    { +        // LLInstanceTracker should respond appropriately if a subclass +        // constructor throws an exception. Specifically, it should run +        // LLInstanceTracker's destructor and remove itself from the +        // underlying container. +        if (! thrw.empty()) +        { +            throw Badness(thrw); +        } +    }  };  /***************************************************************************** @@ -95,6 +112,7 @@ namespace tut      void object::test<2>()      {          ensure_equals(Unkeyed::instanceCount(), 0); +        Unkeyed* dangling = NULL;          {              Unkeyed one;              ensure_equals(Unkeyed::instanceCount(), 1); @@ -107,7 +125,11 @@ namespace tut                  ensure_equals(found, two.get());              }              ensure_equals(Unkeyed::instanceCount(), 1); -        } +            // store an unwise pointer to a temp Unkeyed instance +            dangling = &one; +        } // make that instance vanish +        // check the now-invalid pointer to the destroyed instance +        ensure("getInstance(T*) failed to track destruction", ! Unkeyed::getInstance(dangling));          ensure_equals(Unkeyed::instanceCount(), 0);      } @@ -229,4 +251,49 @@ namespace tut          }          ensure(! what.empty());      } + +    template<> template<> +    void object::test<8>() +    { +        set_test_name("exception in subclass ctor"); +        typedef std::set<Unkeyed*> InstanceSet; +        InstanceSet existing; +        // We can't use the iterator-range InstanceSet constructor because +        // beginInstances() returns an iterator that dereferences to an +        // Unkeyed&, not an Unkeyed*. +        for (Unkeyed::instance_iter uki(Unkeyed::beginInstances()), +                                    ukend(Unkeyed::endInstances()); +             uki != ukend; ++uki) +        { +            existing.insert(&*uki); +        } +        Unkeyed* puk = NULL; +        try +        { +            // We don't expect the assignment to take place because we expect +            // Unkeyed to respond to the non-empty string param by throwing. +            // We know the LLInstanceTracker base-class constructor will have +            // run before Unkeyed's constructor, therefore the new instance +            // will have added itself to the underlying set. The whole +            // question is, when Unkeyed's constructor throws, will +            // LLInstanceTracker's destructor remove it from the set? I +            // realize we're testing the C++ implementation more than +            // Unkeyed's implementation, but this seems an important point to +            // nail down. +            puk = new Unkeyed("throw"); +        } +        catch (const Badness&) +        { +        } +        // Ensure that every member of the new, updated set of Unkeyed +        // instances was also present in the original set. If that's not true, +        // it's because our new Unkeyed ended up in the updated set despite +        // its constructor exception. +        for (Unkeyed::instance_iter uki(Unkeyed::beginInstances()), +                                    ukend(Unkeyed::endInstances()); +             uki != ukend; ++uki) +        { +            ensure("failed to remove instance", existing.find(&*uki) != existing.end()); +        } +    }  } // namespace tut diff --git a/indra/llcommon/tests/llleap_test.cpp b/indra/llcommon/tests/llleap_test.cpp new file mode 100644 index 0000000000..9b755e9ca5 --- /dev/null +++ b/indra/llcommon/tests/llleap_test.cpp @@ -0,0 +1,694 @@ +/** + * @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" +#include <functional> + +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 + +#if ! LL_WINDOWS +const size_t BUFFERED_LENGTH = 1023*1024; // try wrangling just under a megabyte of data +#else +// "Then there's Windows... sigh." The "very large message" test is flaky in a +// way that seems to point to either the OS (nonblocking writes to pipes) or +// possibly the apr_file_write() function. Poring over log messages reveals +// that at some point along the way apr_file_write() returns 11 (Resource +// temporarily unavailable, i.e. EAGAIN) and says it wrote 0 bytes -- even +// though it did write the chunk! Our next write attempt retries the same +// chunk, resulting in the chunk being duplicated at the child end, corrupting +// the data stream. Much as I would love to be able to fix it for real, such a +// fix would appear to require distinguishing bogus EAGAIN returns from real +// ones -- how?? Empirically this behavior is only observed when writing a +// "very large message". To be able to move forward at all, try to bypass this +// particular failure by adjusting the size of a "very large message" on +// Windows. +const size_t BUFFERED_LENGTH = 65336; +#endif  // LL_WINDOWS + +void waitfor(const std::vector<LLLeap*>& instances, int timeout=60) +{ +    int i; +    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) +        { +/*==========================================================================*| +            std::cout << instances.size() << " LLLeap instances terminated in " +                      << i << " seconds, proceeding" << std::endl; +|*==========================================================================*/ +            return; +        } +        // Found an instance that's still running. Wait and pump LLProcess. +        sleep(1); +        LLEventPumps::instance().obtain("mainloop").post(LLSD()); +    } +    tut::ensure(STRINGIZE("at least 1 of " << instances.size() +                          << " LLLeap instances timed out (" +                          << timeout << " seconds) without terminating"), +                i < timeout); +} + +void waitfor(LLLeap* instance, int timeout=60) +{ +    std::vector<LLLeap*> instances; +    instances.push_back(instance); +    waitfor(instances, timeout); +} + +/***************************************************************************** +*   TUT +*****************************************************************************/ +namespace tut +{ +    struct llleap_data +    { +        llleap_data(): +            reader(".py", +                   // This logic is adapted from vita.viewerclient.receiveEvent() +                   boost::lambda::_1 << +                   "import re\n" +                   "import os\n" +                   "import sys\n" +                   "\n" +                   // Don't forget that this Python script is written to some +                   // temp directory somewhere! Its __file__ is useless in +                   // finding indra/lib/python. Use our __FILE__, with +                   // raw-string syntax to deal with Windows pathnames. +                   "mydir = os.path.dirname(r'" << __FILE__ << "')\n" +                   "try:\n" +                   "    from llbase import llsd\n" +                   "except ImportError:\n" +                   // We expect mydir to be .../indra/llcommon/tests. +                   "    sys.path.insert(0,\n" +                   "        os.path.join(mydir, os.pardir, os.pardir, 'lib', 'python'))\n" +                   "    from indra.base import llsd\n" +                   "\n" +                   "class ProtocolError(Exception):\n" +                   "    def __init__(self, msg, data):\n" +                   "        Exception.__init__(self, msg)\n" +                   "        self.data = data\n" +                   "\n" +                   "class ParseError(ProtocolError):\n" +                   "    pass\n" +                   "\n" +                   "def get():\n" +                   "    hdr = ''\n" +                   "    while ':' not in hdr and len(hdr) < 20:\n" +                   "        hdr += sys.stdin.read(1)\n" +                   "        if not hdr:\n" +                   "            sys.exit(0)\n" +                   "    if not hdr.endswith(':'):\n" +                   "        raise ProtocolError('Expected len:data, got %r' % hdr, hdr)\n" +                   "    try:\n" +                   "        length = int(hdr[:-1])\n" +                   "    except ValueError:\n" +                   "        raise ProtocolError('Non-numeric len %r' % hdr[:-1], hdr[:-1])\n" +                   "    parts = []\n" +                   "    received = 0\n" +                   "    while received < length:\n" +                   "        parts.append(sys.stdin.read(length - received))\n" +                   "        received += len(parts[-1])\n" +                   "    data = ''.join(parts)\n" +                   "    assert len(data) == length\n" +                   "    try:\n" +                   "        return llsd.parse(data)\n" +                   //   Seems the old indra.base.llsd module didn't properly +                   //   convert IndexError (from running off end of string) to +                   //   LLSDParseError. +                   "    except (IndexError, llsd.LLSDParseError), e:\n" +                   "        msg = 'Bad received packet (%s)' % e\n" +                   "        print >>sys.stderr, '%s, %s bytes:' % (msg, len(data))\n" +                   "        showmax = 40\n" +                   //       We've observed failures with very large packets; +                   //       dumping the entire packet wastes time and space. +                   //       But if the error states a particular byte offset, +                   //       truncate to (near) that offset when dumping data. +                   "        location = re.search(r' at (byte|index) ([0-9]+)', str(e))\n" +                   "        if not location:\n" +                   "            # didn't find offset, dump whole thing, no ellipsis\n" +                   "            ellipsis = ''\n" +                   "        else:\n" +                   "            # found offset within error message\n" +                   "            trunc = int(location.group(2)) + showmax\n" +                   "            data = data[:trunc]\n" +                   "            ellipsis = '... (%s more)' % (length - trunc)\n" +                   "        offset = -showmax\n" +                   "        for offset in xrange(0, len(data)-showmax, showmax):\n" +                   "            print >>sys.stderr, '%04d: %r +' % \\\n" +                   "                  (offset, data[offset:offset+showmax])\n" +                   "        offset += showmax\n" +                   "        print >>sys.stderr, '%04d: %r%s' % \\\n" +                   "              (offset, data[offset:], ellipsis)\n" +                   "        raise ParseError(msg, data)\n" +                   "\n" +                   "# deal with initial stdin message\n" +                   // this will throw if the initial write to stdin doesn't +                   // follow len:data protocol, or if we couldn't find 'pump' +                   // in the dict +                   "_reply = get()['pump']\n" +                   "\n" +                   "def replypump():\n" +                   "    return _reply\n" +                   "\n" +                   "def put(req):\n" +                   "    sys.stdout.write(':'.join((str(len(req)), req)))\n" +                   "    sys.stdout.flush()\n" +                   "\n" +                   "def send(pump, data):\n" +                   "    put(llsd.format_notation(dict(pump=pump, data=data)))\n" +                   "\n" +                   "def request(pump, data):\n" +                   "    # we expect 'data' is a dict\n" +                   "    data['reply'] = _reply\n" +                   "    send(pump, 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("bad stdout protocol"); +        NamedTempFile script("py", +                             "print 'Hello from Python!'\n"); +        CaptureLog log(LLError::LEVEL_WARN); +        waitfor(LLLeap::create(get_test_name(), +                               sv(list_of(PYTHON)(script.getName())))); +        ensure_contains("error log line", +                        log.messageWith("invalid protocol"), "Hello from Python!"); +    } + +    template<> template<> +    void object::test<4>() +    { +        set_test_name("leftover stdout"); +        NamedTempFile script("py", +                             "import sys\n" +                             // note lack of newline +                             "sys.stdout.write('Hello from Python!')\n"); +        CaptureLog log(LLError::LEVEL_WARN); +        waitfor(LLLeap::create(get_test_name(), +                               sv(list_of(PYTHON)(script.getName())))); +        ensure_contains("error log line", +                        log.messageWith("Discarding"), "Hello from Python!"); +    } + +    template<> template<> +    void object::test<5>() +    { +        set_test_name("bad stdout len prefix"); +        NamedTempFile script("py", +                             "import sys\n" +                             "sys.stdout.write('5a2:something')\n"); +        CaptureLog log(LLError::LEVEL_WARN); +        waitfor(LLLeap::create(get_test_name(), +                               sv(list_of(PYTHON)(script.getName())))); +        ensure_contains("error log line", +                        log.messageWith("invalid protocol"), "5a2:"); +    } + +    template<> template<> +    void object::test<6>() +    { +        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<7>() +    { +        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)); +    } + +    // Generic self-contained listener: derive from this and override its +    // call() method, then tell somebody to post on the pump named getName(). +    // Control will reach your call() override. +    struct ListenerBase +    { +        // Pass the pump name you want; will tweak for uniqueness. +        ListenerBase(const std::string& name): +            mPump(name, true) +        { +            mPump.listen(name, boost::bind(&ListenerBase::call, this, _1)); +        } + +        virtual ~ListenerBase() {}  // pacify MSVC + +        virtual bool call(const LLSD& request) +        { +            return false; +        } + +        LLEventPump& getPump() { return mPump; } +        const LLEventPump& getPump() const { return mPump; } + +        std::string getName() const { return mPump.getName(); } +        void post(const LLSD& data) { mPump.post(data); } + +        LLEventStream mPump; +    }; + +    // Mimic a dummy little LLEventAPI that merely sends a reply back to its +    // requester on the "reply" pump. +    struct AckAPI: public ListenerBase +    { +        AckAPI(): ListenerBase("AckAPI") {} + +        virtual bool call(const LLSD& request) +        { +            LLEventPumps::instance().obtain(request["reply"]).post("ack"); +            return false; +        } +    }; + +    // Give LLLeap script a way to post success/failure. +    struct Result: public ListenerBase +    { +        Result(): ListenerBase("Result") {} + +        virtual bool call(const LLSD& request) +        { +            mData = request; +            return false; +        } + +        void ensure() const +        { +            tut::ensure(std::string("never posted to ") + getName(), mData.isDefined()); +            // Post an empty string for success, non-empty string is failure message. +            tut::ensure(mData, mData.asString().empty()); +        } + +        LLSD mData; +    }; + +    template<> template<> +    void object::test<8>() +    { +        set_test_name("round trip"); +        AckAPI api; +        Result result; +        NamedTempFile script("py", +                             boost::lambda::_1 << +                             "from " << reader_module << " import *\n" +                             // make a request on our little API +                             "request(pump='" << api.getName() << "', data={})\n" +                             // wait for its response +                             "resp = get()\n" +                             "result = '' if resp == dict(pump=replypump(), data='ack')\\\n" +                             "            else 'bad: ' + str(resp)\n" +                             "send(pump='" << result.getName() << "', data=result)\n"); +        waitfor(LLLeap::create(get_test_name(), sv(list_of(PYTHON)(script.getName())))); +        result.ensure(); +    } + +    struct ReqIDAPI: public ListenerBase +    { +        ReqIDAPI(): ListenerBase("ReqIDAPI") {} + +        virtual bool call(const LLSD& request) +        { +            // free function from llevents.h +            sendReply(LLSD(), request); +            return false; +        } +    }; + +    template<> template<> +    void object::test<9>() +    { +        set_test_name("many small messages"); +        // It's not clear to me whether there's value in iterating many times +        // over a send/receive loop -- I don't think that will exercise any +        // interesting corner cases. This test first sends a large number of +        // messages, then receives all the responses. The intent is to ensure +        // that some of that data stream crosses buffer boundaries, loop +        // iterations etc. in OS pipes and the LLLeap/LLProcess implementation. +        ReqIDAPI api; +        Result result; +        NamedTempFile script("py", +                             boost::lambda::_1 << +                             "import sys\n" +                             "from " << reader_module << " import *\n" +                             // Note that since reader imports llsd, this +                             // 'import *' gets us llsd too. +                             "sample = llsd.format_notation(dict(pump='" << +                             api.getName() << "', data=dict(reqid=999999, reply=replypump())))\n" +                             // The whole packet has length prefix too: "len:data" +                             "samplen = len(str(len(sample))) + 1 + len(sample)\n" +                             // guess how many messages it will take to +                             // accumulate BUFFERED_LENGTH +                             "count = int(" << BUFFERED_LENGTH << "/samplen)\n" +                             "print >>sys.stderr, 'Sending %s requests' % count\n" +                             "for i in xrange(count):\n" +                             "    request('" << api.getName() << "', dict(reqid=i))\n" +                             // The assumption in this specific test that +                             // replies will arrive in the same order as +                             // requests is ONLY valid because the API we're +                             // invoking sends replies instantly. If the API +                             // had to wait for some external event before +                             // sending its reply, replies could arrive in +                             // arbitrary order, and we'd have to tick them +                             // off from a set. +                             "result = ''\n" +                             "for i in xrange(count):\n" +                             "    resp = get()\n" +                             "    if resp['data']['reqid'] != i:\n" +                             "        result = 'expected reqid=%s in %s' % (i, resp)\n" +                             "        break\n" +                             "send(pump='" << result.getName() << "', data=result)\n"); +        waitfor(LLLeap::create(get_test_name(), sv(list_of(PYTHON)(script.getName()))), +                300);               // needs more realtime than most tests +        result.ensure(); +    } + +    // This is the body of test<10>, extracted so we can run it over a number +    // of large-message sizes. +    void test_large_message(const std::string& PYTHON, const std::string& reader_module, +                            const std::string& test_name, size_t size) +    { +        ReqIDAPI api; +        Result result; +        NamedTempFile script("py", +                             boost::lambda::_1 << +                             "import sys\n" +                             "from " << reader_module << " import *\n" +                             // Generate a very large string value. +                             "desired = int(sys.argv[1])\n" +                             // 7 chars per item: 6 digits, 1 comma +                             "count = int((desired - 50)/7)\n" +                             "large = ''.join('%06d,' % i for i in xrange(count))\n" +                             // Pass 'large' as reqid because we know the API +                             // will echo reqid, and we want to receive it back. +                             "request('" << api.getName() << "', dict(reqid=large))\n" +                             "try:\n" +                             "    resp = get()\n" +                             "except ParseError, e:\n" +                             "    # try to find where e.data diverges from expectation\n" +                             // Normally we'd expect a 'pump' key in there, +                             // too, with value replypump(). But Python +                             // serializes keys in a different order than C++, +                             // so incoming data start with 'data'. +                             // Truthfully, though, if we get as far as 'pump' +                             // before we find a difference, something's very +                             // strange. +                             "    expect = llsd.format_notation(dict(data=dict(reqid=large)))\n" +                             "    chunk = 40\n" +                             "    for offset in xrange(0, max(len(e.data), len(expect)), chunk):\n" +                             "        if e.data[offset:offset+chunk] != \\\n" +                             "           expect[offset:offset+chunk]:\n" +                             "            print >>sys.stderr, 'Offset %06d: expect %r,\\n'\\\n" +                             "                                '                  get %r' %\\\n" +                             "                                (offset,\n" +                             "                                 expect[offset:offset+chunk],\n" +                             "                                 e.data[offset:offset+chunk])\n" +                             "            break\n" +                             "    else:\n" +                             "        print >>sys.stderr, 'incoming data matches expect?!'\n" +                             "    send('" << result.getName() << "', '%s: %s' % (e.__class__.__name__, e))\n" +                             "    sys.exit(1)\n" +                             "\n" +                             "echoed = resp['data']['reqid']\n" +                             "if echoed == large:\n" +                             "    send('" << result.getName() << "', '')\n" +                             "    sys.exit(0)\n" +                             // Here we know echoed did NOT match; try to find where +                             "for i in xrange(count):\n" +                             "    start = 7*i\n" +                             "    end   = 7*(i+1)\n" +                             "    if end > len(echoed)\\\n" +                             "    or echoed[start:end] != large[start:end]:\n" +                             "        send('" << result.getName() << "',\n" +                             "             'at offset %s, expected %r but got %r' %\n" +                             "             (start, large[start:end], echoed[start:end]))\n" +                             "sys.exit(1)\n"); +        waitfor(LLLeap::create(test_name, +                               sv(list_of +                                  (PYTHON) +                                  (script.getName()) +                                  (stringize(size)))), +                180);               // try a longer timeout +        result.ensure(); +    } + +    struct TestLargeMessage: public std::binary_function<size_t, size_t, bool> +    { +        TestLargeMessage(const std::string& PYTHON_, const std::string& reader_module_, +                         const std::string& test_name_): +            PYTHON(PYTHON_), +            reader_module(reader_module_), +            test_name(test_name_) +        {} + +        bool operator()(size_t left, size_t right) const +        { +            // We don't know whether upper_bound is going to pass the "sought +            // value" as the left or the right operand. We pass 0 as the +            // "sought value" so we can distinguish it. Of course that means +            // the sequence we're searching must not itself contain 0! +            size_t size; +            bool success; +            if (left) +            { +                size = left; +                // Consider our return value carefully. Normal binary_search +                // (or, in our case, upper_bound) expects a container sorted +                // in ascending order, and defaults to the std::less +                // comparator. Our container is in fact in ascending order, so +                // return consistently with std::less. Here we were called as +                // compare(item, sought). If std::less were called that way, +                // 'true' would mean to move right (to higher numbers) within +                // the sequence: the item being considered is less than the +                // sought value. For us, that means that test_large_message() +                // success should return 'true'. +                success = true; +            } +            else +            { +                size = right; +                // Here we were called as compare(sought, item). If std::less +                // were called that way, 'true' would mean to move left (to +                // lower numbers) within the sequence: the sought value is +                // less than the item being considered. For us, that means +                // test_large_message() FAILURE should return 'true', hence +                // test_large_message() success should return 'false'. +                success = false; +            } + +            try +            { +                test_large_message(PYTHON, reader_module, test_name, size); +                std::cout << "test_large_message(" << size << ") succeeded" << std::endl; +                return success; +            } +            catch (const failure& e) +            { +                std::cout << "test_large_message(" << size << ") failed: " << e.what() << std::endl; +                return ! success; +            } +        } + +        const std::string PYTHON, reader_module, test_name; +    }; + +    // The point of this function is to try to find a size at which +    // test_large_message() can succeed. We still want the overall test to +    // fail; otherwise we won't get the coder's attention -- but if +    // test_large_message() fails, try to find a plausible size at which it +    // DOES work. +    void test_or_split(const std::string& PYTHON, const std::string& reader_module, +                       const std::string& test_name, size_t size) +    { +        try +        { +            test_large_message(PYTHON, reader_module, test_name, size); +        } +        catch (const failure& e) +        { +            std::cout << "test_large_message(" << size << ") failed: " << e.what() << std::endl; +            // If it still fails below 4K, give up: subdividing any further is +            // pointless. +            if (size >= 4096) +            { +                try +                { +                    // Recur with half the size +                    size_t smaller(size/2); +                    test_or_split(PYTHON, reader_module, test_name, smaller); +                    // Recursive call will throw if test_large_message() +                    // failed, therefore we only reach the line below if it +                    // succeeded. +                    std::cout << "but test_large_message(" << smaller << ") succeeded" << std::endl; + +                    // Binary search for largest size that works. But since +                    // std::binary_search() only returns bool, actually use +                    // std::upper_bound(), consistent with our desire to find +                    // the LARGEST size that works. First generate a sorted +                    // container of all the sizes we intend to try, from +                    // 'smaller' (known to work) to 'size' (known to fail). We +                    // could whomp up magic iterators to do this dynamically, +                    // without actually instantiating a vector, but for a test +                    // program this will do. At least preallocate the vector. +                    // Per TestLargeMessage comments, it's important that this +                    // vector not contain 0. +                    std::vector<size_t> sizes; +                    sizes.reserve((size - smaller)/4096 + 1); +                    for (size_t sz(smaller), szend(size); sz < szend; sz += 4096) +                        sizes.push_back(sz); +                    // our comparator +                    TestLargeMessage tester(PYTHON, reader_module, test_name); +                    // Per TestLargeMessage comments, pass 0 as the sought value. +                    std::vector<size_t>::const_iterator found = +                        std::upper_bound(sizes.begin(), sizes.end(), 0, tester); +                    if (found != sizes.end() && found != sizes.begin()) +                    { +                        std::cout << "test_large_message(" << *(found - 1) +                                  << ") is largest that succeeds" << std::endl; +                    } +                    else +                    { +                        std::cout << "cannot determine largest test_large_message(size) " +                                  << "that succeeds" << std::endl; +                    } +                } +                catch (const failure&) +                { +                    // The recursive test_or_split() call above has already +                    // handled the exception. We don't want our caller to see +                    // innermost exception; propagate outermost (below). +                } +            } +            // In any case, because we reached here through failure of +            // our original test_large_message(size) call, ensure failure +            // propagates. +            throw e; +        } +    } + +    template<> template<> +    void object::test<10>() +    { +        set_test_name("very large message"); +        test_or_split(PYTHON, reader_module, get_test_name(), BUFFERED_LENGTH); +    } +} // namespace tut diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp new file mode 100644 index 0000000000..99186ed434 --- /dev/null +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -0,0 +1,1262 @@ +/** + * @file   llprocess_test.cpp + * @author Nat Goodspeed + * @date   2011-12-19 + * @brief  Test for llprocess. + *  + * $LicenseInfo:firstyear=2011&license=viewerlgpl$ + * Copyright (c) 2011, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "llprocess.h" +// STL headers +#include <vector> +#include <list> +// std headers +#include <fstream> +// external library headers +#include "llapr.h" +#include "apr_thread_proc.h" +#include <boost/foreach.hpp> +#include <boost/function.hpp> +#include <boost/algorithm/string/find_iterator.hpp> +#include <boost/algorithm/string/finder.hpp> +//#include <boost/lambda/lambda.hpp> +//#include <boost/lambda/bind.hpp> +// other Linden headers +#include "../test/lltut.h" +#include "../test/manageapr.h" +#include "../test/namedtempfile.h" +#include "../test/catch_and_store_what_in.h" +#include "stringize.h" +#include "llsdutil.h" +#include "llevents.h" +#include "wrapllerrs.h" + +#if defined(LL_WINDOWS) +#define sleep(secs) _sleep((secs) * 1000) +#define EOL "\r\n" +#else +#define EOL "\n" +#include <sys/wait.h> +#endif + +//namespace lambda = boost::lambda; + +// static instance of this manages APR init/cleanup +static ManageAPR manager; + +/***************************************************************************** +*   Helpers +*****************************************************************************/ + +#define ensure_equals_(left, right) \ +        ensure_equals(STRINGIZE(#left << " != " << #right), (left), (right)) + +#define aprchk(expr) aprchk_(#expr, (expr)) +static void aprchk_(const char* call, apr_status_t rv, apr_status_t expected=APR_SUCCESS) +{ +    tut::ensure_equals(STRINGIZE(call << " => " << rv << ": " << manager.strerror(rv)), +                       rv, expected); +} + +/** + * Read specified file using std::getline(). It is assumed to be an error if + * the file is empty: don't use this function if that's an acceptable case. + * Last line will not end with '\n'; this is to facilitate the usual case of + * string compares with a single line of output. + * @param pathname The file to read. + * @param desc Optional description of the file for error message; + * defaults to "in <pathname>" + */ +static std::string readfile(const std::string& pathname, const std::string& desc="") +{ +    std::string use_desc(desc); +    if (use_desc.empty()) +    { +        use_desc = STRINGIZE("in " << pathname); +    } +    std::ifstream inf(pathname.c_str()); +    std::string output; +    tut::ensure(STRINGIZE("No output " << use_desc), std::getline(inf, output)); +    std::string more; +    while (std::getline(inf, more)) +    { +        output += '\n' + more; +    } +    return output; +} + +/// Looping on LLProcess::isRunning() must now be accompanied by pumping +/// "mainloop" -- otherwise the status won't update and you get an infinite +/// loop. +void yield(int seconds=1) +{ +    // This function simulates waiting for another viewer frame +    sleep(seconds); +    LLEventPumps::instance().obtain("mainloop").post(LLSD()); +} + +void waitfor(LLProcess& proc, int timeout=60) +{ +    int i = 0; +    for ( ; i < timeout && proc.isRunning(); ++i) +    { +        yield(); +    } +    tut::ensure(STRINGIZE("process took longer than " << timeout << " seconds to terminate"), +                i < timeout); +} + +void waitfor(LLProcess::handle h, const std::string& desc, int timeout=60) +{ +    int i = 0; +    for ( ; i < timeout && LLProcess::isRunning(h, desc); ++i) +    { +        yield(); +    } +    tut::ensure(STRINGIZE("process took longer than " << timeout << " seconds to terminate"), +                i < timeout); +} + +/** + * Construct an LLProcess to run a Python script. + */ +struct PythonProcessLauncher +{ +    /** +     * @param desc Arbitrary description for error messages +     * @param script Python script, any form acceptable to NamedTempFile, +     * typically either a std::string or an expression of the form +     * (lambda::_1 << "script content with " << variable_data) +     */ +    template <typename CONTENT> +    PythonProcessLauncher(const std::string& desc, const CONTENT& script): +        mDesc(desc), +        mScript("py", script) +    { +        const char* PYTHON(getenv("PYTHON")); +        tut::ensure("Set $PYTHON to the Python interpreter", PYTHON); + +        mParams.desc = desc + " script"; +        mParams.executable = PYTHON; +        mParams.args.add(mScript.getName()); +    } + +    /// Launch Python script; verify that it launched +    void launch() +    { +        mPy = LLProcess::create(mParams); +        tut::ensure(STRINGIZE("Couldn't launch " << mDesc << " script"), mPy); +    } + +    /// Run Python script and wait for it to complete. +    void run() +    { +        launch(); +        // One of the irritating things about LLProcess is that +        // there's no API to wait for the child to terminate -- but given +        // its use in our graphics-intensive interactive viewer, it's +        // understandable. +        waitfor(*mPy); +    } + +    /** +     * Run a Python script using LLProcess, expecting that it will +     * write to the file passed as its sys.argv[1]. Retrieve that output. +     * +     * Until January 2012, LLProcess provided distressingly few +     * mechanisms for a child process to communicate back to its caller -- +     * not even its return code. We've introduced a convention by which we +     * create an empty temp file, pass the name of that file to our child +     * as sys.argv[1] and expect the script to write its output to that +     * file. This function implements the C++ (parent process) side of +     * that convention. +     */ +    std::string run_read() +    { +        NamedTempFile out("out", ""); // placeholder +        // pass name of this temporary file to the script +        mParams.args.add(out.getName()); +        run(); +        // assuming the script wrote to that file, read it +        return readfile(out.getName(), STRINGIZE("from " << mDesc << " script")); +    } + +    LLProcess::Params mParams; +    LLProcessPtr mPy; +    std::string mDesc; +    NamedTempFile mScript; +}; + +/// convenience function for PythonProcessLauncher::run() +template <typename CONTENT> +static void python(const std::string& desc, const CONTENT& script) +{ +    PythonProcessLauncher py(desc, script); +    py.run(); +} + +/// convenience function for PythonProcessLauncher::run_read() +template <typename CONTENT> +static std::string python_out(const std::string& desc, const CONTENT& script) +{ +    PythonProcessLauncher py(desc, script); +    return py.run_read(); +} + +/// Create a temporary directory and clean it up later. +class NamedTempDir: public boost::noncopyable +{ +public: +    // Use python() function to create a temp directory: I've found +    // nothing in either Boost.Filesystem or APR quite like Python's +    // tempfile.mkdtemp(). +    // Special extra bonus: on Mac, mkdtemp() reports a pathname +    // starting with /var/folders/something, whereas that's really a +    // symlink to /private/var/folders/something. Have to use +    // realpath() to compare properly. +    NamedTempDir(): +        mPath(python_out("mkdtemp()", +                         "from __future__ import with_statement\n" +                         "import os.path, sys, tempfile\n" +                         "with open(sys.argv[1], 'w') as f:\n" +                         "    f.write(os.path.normcase(os.path.normpath(os.path.realpath(tempfile.mkdtemp()))))\n")) +    {} + +    ~NamedTempDir() +    { +        aprchk(apr_dir_remove(mPath.c_str(), gAPRPoolp)); +    } + +    std::string getName() const { return mPath; } + +private: +    std::string mPath; +}; + +/***************************************************************************** +*   TUT +*****************************************************************************/ +namespace tut +{ +    struct llprocess_data +    { +        LLAPRPool pool; +    }; +    typedef test_group<llprocess_data> llprocess_group; +    typedef llprocess_group::object object; +    llprocess_group llprocessgrp("llprocess"); + +    struct Item +    { +        Item(): tries(0) {} +        unsigned    tries; +        std::string which; +        std::string what; +    }; + +/*==========================================================================*| +#define tabent(symbol) { symbol, #symbol } +    static struct ReasonCode +    { +        int code; +        const char* name; +    } reasons[] = +    { +        tabent(APR_OC_REASON_DEATH), +        tabent(APR_OC_REASON_UNWRITABLE), +        tabent(APR_OC_REASON_RESTART), +        tabent(APR_OC_REASON_UNREGISTER), +        tabent(APR_OC_REASON_LOST), +        tabent(APR_OC_REASON_RUNNING) +    }; +#undef tabent +|*==========================================================================*/ + +    struct WaitInfo +    { +        WaitInfo(apr_proc_t* child_): +            child(child_), +            rv(-1),                 // we haven't yet called apr_proc_wait() +            rc(0), +            why(apr_exit_why_e(0)) +        {} +        apr_proc_t* child;          // which subprocess +        apr_status_t rv;            // return from apr_proc_wait() +        int rc;                     // child's exit code +        apr_exit_why_e why;         // APR_PROC_EXIT, APR_PROC_SIGNAL, APR_PROC_SIGNAL_CORE +    }; + +    void child_status_callback(int reason, void* data, int status) +    { +/*==========================================================================*| +        std::string reason_str; +        BOOST_FOREACH(const ReasonCode& rcp, reasons) +        { +            if (reason == rcp.code) +            { +                reason_str = rcp.name; +                break; +            } +        } +        if (reason_str.empty()) +        { +            reason_str = STRINGIZE("unknown reason " << reason); +        } +        std::cout << "child_status_callback(" << reason_str << ")\n"; +|*==========================================================================*/ + +        if (reason == APR_OC_REASON_DEATH || reason == APR_OC_REASON_LOST) +        { +            // Somewhat oddly, APR requires that you explicitly unregister +            // even when it already knows the child has terminated. +            apr_proc_other_child_unregister(data); + +            WaitInfo* wi(static_cast<WaitInfo*>(data)); +            // It's just wrong to call apr_proc_wait() here. The only way APR +            // knows to call us with APR_OC_REASON_DEATH is that it's already +            // reaped this child process, so calling wait() will only produce +            // "huh?" from the OS. We must rely on the status param passed in, +            // which unfortunately comes straight from the OS wait() call. +//          wi->rv = apr_proc_wait(wi->child, &wi->rc, &wi->why, APR_NOWAIT); +            wi->rv = APR_CHILD_DONE; // fake apr_proc_wait() results +#if defined(LL_WINDOWS) +            wi->why = APR_PROC_EXIT; +            wi->rc  = status;         // no encoding on Windows (no signals) +#else  // Posix +            if (WIFEXITED(status)) +            { +                wi->why = APR_PROC_EXIT; +                wi->rc  = WEXITSTATUS(status); +            } +            else if (WIFSIGNALED(status)) +            { +                wi->why = APR_PROC_SIGNAL; +                wi->rc  = WTERMSIG(status); +            } +            else                    // uh, shouldn't happen? +            { +                wi->why = APR_PROC_EXIT; +                wi->rc  = status;   // someone else will have to decode +            } +#endif // Posix +        } +    } + +    template<> template<> +    void object::test<1>() +    { +        set_test_name("raw APR nonblocking I/O"); + +        // Create a script file in a temporary place. +        NamedTempFile script("py", +            "import sys" EOL +            "import time" EOL +            EOL +            "time.sleep(2)" EOL +            "print >>sys.stdout, 'stdout after wait'" EOL +            "sys.stdout.flush()" EOL +            "time.sleep(2)" EOL +            "print >>sys.stderr, 'stderr after wait'" EOL +            "sys.stderr.flush()" EOL +            ); + +        // Arrange to track the history of our interaction with child: what we +        // fetched, which pipe it came from, how many tries it took before we +        // got it. +        std::vector<Item> history; +        history.push_back(Item()); + +        // Run the child process. +        apr_procattr_t *procattr = NULL; +        aprchk(apr_procattr_create(&procattr, pool.getAPRPool())); +        aprchk(apr_procattr_io_set(procattr, APR_CHILD_BLOCK, APR_CHILD_BLOCK, APR_CHILD_BLOCK)); +        aprchk(apr_procattr_cmdtype_set(procattr, APR_PROGRAM_PATH)); + +        std::vector<const char*> argv; +        apr_proc_t child; +        argv.push_back("python"); +        // Have to have a named copy of this std::string so its c_str() value +        // will persist. +        std::string scriptname(script.getName()); +        argv.push_back(scriptname.c_str()); +        argv.push_back(NULL); + +        aprchk(apr_proc_create(&child, argv[0], +                               &argv[0], +                               NULL, // if we wanted to pass explicit environment +                               procattr, +                               pool.getAPRPool())); + +        // We do not want this child process to outlive our APR pool. On +        // destruction of the pool, forcibly kill the process. Tell APR to try +        // SIGTERM and wait 3 seconds. If that didn't work, use SIGKILL. +        apr_pool_note_subprocess(pool.getAPRPool(), &child, APR_KILL_AFTER_TIMEOUT); + +        // arrange to call child_status_callback() +        WaitInfo wi(&child); +        apr_proc_other_child_register(&child, child_status_callback, &wi, child.in, pool.getAPRPool()); + +        // TODO: +        // Stuff child.in until it (would) block to verify EWOULDBLOCK/EAGAIN. +        // Have child script clear it later, then write one more line to prove +        // that it gets through. + +        // Monitor two different output pipes. Because one will be closed +        // before the other, keep them in a list so we can drop whichever of +        // them is closed first. +        typedef std::pair<std::string, apr_file_t*> DescFile; +        typedef std::list<DescFile> DescFileList; +        DescFileList outfiles; +        outfiles.push_back(DescFile("out", child.out)); +        outfiles.push_back(DescFile("err", child.err)); + +        while (! outfiles.empty()) +        { +            // This peculiar for loop is designed to let us erase(dfli). With +            // a list, that invalidates only dfli itself -- but even so, we +            // lose the ability to increment it for the next item. So at the +            // top of every loop, while dfli is still valid, increment +            // dflnext. Then before the next iteration, set dfli to dflnext. +            for (DescFileList::iterator +                     dfli(outfiles.begin()), dflnext(outfiles.begin()), dflend(outfiles.end()); +                 dfli != dflend; dfli = dflnext) +            { +                // Only valid to increment dflnext once we're sure it's not +                // already at dflend. +                ++dflnext; + +                char buf[4096]; + +                apr_status_t rv = apr_file_gets(buf, sizeof(buf), dfli->second); +                if (APR_STATUS_IS_EOF(rv)) +                { +//                  std::cout << "(EOF on " << dfli->first << ")\n"; +//                  history.back().which = dfli->first; +//                  history.back().what  = "*eof*"; +//                  history.push_back(Item()); +                    outfiles.erase(dfli); +                    continue; +                } +                if (rv == EWOULDBLOCK || rv == EAGAIN) +                { +//                  std::cout << "(waiting; apr_file_gets(" << dfli->first << ") => " << rv << ": " << manager.strerror(rv) << ")\n"; +                    ++history.back().tries; +                    continue; +                } +                aprchk_("apr_file_gets(buf, sizeof(buf), dfli->second)", rv); +                // Is it even possible to get APR_SUCCESS but read 0 bytes? +                // Hope not, but defend against that anyway. +                if (buf[0]) +                { +//                  std::cout << dfli->first << ": " << buf; +                    history.back().which = dfli->first; +                    history.back().what.append(buf); +                    if (buf[strlen(buf) - 1] == '\n') +                        history.push_back(Item()); +                    else +                    { +                        // Just for pretty output... if we only read a partial +                        // line, terminate it. +//                      std::cout << "...\n"; +                    } +                } +            } +            // Do this once per tick, as we expect the viewer will +            apr_proc_other_child_refresh_all(APR_OC_REASON_RUNNING); +            sleep(1); +        } +        apr_file_close(child.in); +        apr_file_close(child.out); +        apr_file_close(child.err); + +        // Okay, we've broken the loop because our pipes are all closed. If we +        // haven't yet called wait, give the callback one more chance. This +        // models the fact that unlike this small test program, the viewer +        // will still be running. +        if (wi.rv == -1) +        { +            std::cout << "last gasp apr_proc_other_child_refresh_all()\n"; +            apr_proc_other_child_refresh_all(APR_OC_REASON_RUNNING); +        } + +        if (wi.rv == -1) +        { +            std::cout << "child_status_callback(APR_OC_REASON_DEATH) wasn't called" << std::endl; +            wi.rv = apr_proc_wait(wi.child, &wi.rc, &wi.why, APR_NOWAIT); +        } +//      std::cout << "child done: rv = " << rv << " (" << manager.strerror(rv) << "), why = " << why << ", rc = " << rc << '\n'; +        aprchk_("apr_proc_wait(wi->child, &wi->rc, &wi->why, APR_NOWAIT)", wi.rv, APR_CHILD_DONE); +        ensure_equals_(wi.why, APR_PROC_EXIT); +        ensure_equals_(wi.rc, 0); + +        // Beyond merely executing all the above successfully, verify that we +        // obtained expected output -- and that we duly got control while +        // waiting, proving the non-blocking nature of these pipes. +        try +        { +            unsigned i = 0; +            ensure("blocking I/O on child pipe (0)", history[i].tries); +            ensure_equals_(history[i].which, "out"); +            ensure_equals_(history[i].what,  "stdout after wait" EOL); +//          ++i; +//          ensure_equals_(history[i].which, "out"); +//          ensure_equals_(history[i].what,  "*eof*"); +            ++i; +            ensure("blocking I/O on child pipe (1)", history[i].tries); +            ensure_equals_(history[i].which, "err"); +            ensure_equals_(history[i].what,  "stderr after wait" EOL); +//          ++i; +//          ensure_equals_(history[i].which, "err"); +//          ensure_equals_(history[i].what,  "*eof*"); +        } +        catch (const failure&) +        { +            std::cout << "History:\n"; +            BOOST_FOREACH(const Item& item, history) +            { +                std::string what(item.what); +                if ((! what.empty()) && what[what.length() - 1] == '\n') +                { +                    what.erase(what.length() - 1); +                    if ((! what.empty()) && what[what.length() - 1] == '\r') +                    { +                        what.erase(what.length() - 1); +                        what.append("\\r"); +                    } +                    what.append("\\n"); +                } +                std::cout << "  " << item.which << ": '" << what << "' (" +                          << item.tries << " tries)\n"; +            } +            std::cout << std::flush; +            // re-raise same error; just want to enrich the output +            throw; +        } +    } + +    template<> template<> +    void object::test<2>() +    { +        set_test_name("setWorkingDirectory()"); +        // We want to test setWorkingDirectory(). But what directory is +        // guaranteed to exist on every machine, under every OS? Have to +        // create one. Naturally, ensure we clean it up when done. +        NamedTempDir tempdir; +        PythonProcessLauncher py(get_test_name(), +                                 "from __future__ import with_statement\n" +                                 "import os, sys\n" +                                 "with open(sys.argv[1], 'w') as f:\n" +                                 "    f.write(os.path.normcase(os.path.normpath(os.getcwd())))\n"); +        // Before running, call setWorkingDirectory() +        py.mParams.cwd = tempdir.getName(); +        ensure_equals("os.getcwd()", py.run_read(), tempdir.getName()); +    } + +    template<> template<> +    void object::test<3>() +    { +        set_test_name("arguments"); +        PythonProcessLauncher py(get_test_name(), +                                 "from __future__ import with_statement\n" +                                 "import sys\n" +                                 // note nonstandard output-file arg! +                                 "with open(sys.argv[3], 'w') as f:\n" +                                 "    for arg in sys.argv[1:]:\n" +                                 "        print >>f, arg\n"); +        // We expect that PythonProcessLauncher has already appended +        // its own NamedTempFile to mParams.args (sys.argv[0]). +        py.mParams.args.add("first arg");          // sys.argv[1] +        py.mParams.args.add("second arg");         // sys.argv[2] +        // run_read() appends() one more argument, hence [3] +        std::string output(py.run_read()); +        boost::split_iterator<std::string::const_iterator> +            li(output, boost::first_finder("\n")), lend; +        ensure("didn't get first arg", li != lend); +        std::string arg(li->begin(), li->end()); +        ensure_equals(arg, "first arg"); +        ++li; +        ensure("didn't get second arg", li != lend); +        arg.assign(li->begin(), li->end()); +        ensure_equals(arg, "second arg"); +        ++li; +        ensure("didn't get output filename?!", li != lend); +        arg.assign(li->begin(), li->end()); +        ensure("output filename empty?!", ! arg.empty()); +        ++li; +        ensure("too many args", li == lend); +    } + +    template<> template<> +    void object::test<4>() +    { +        set_test_name("exit(0)"); +        PythonProcessLauncher py(get_test_name(), +                                 "import sys\n" +                                 "sys.exit(0)\n"); +        py.run(); +        ensure_equals("Status.mState", py.mPy->getStatus().mState, LLProcess::EXITED); +        ensure_equals("Status.mData",  py.mPy->getStatus().mData,  0); +    } + +    template<> template<> +    void object::test<5>() +    { +        set_test_name("exit(2)"); +        PythonProcessLauncher py(get_test_name(), +                                 "import sys\n" +                                 "sys.exit(2)\n"); +        py.run(); +        ensure_equals("Status.mState", py.mPy->getStatus().mState, LLProcess::EXITED); +        ensure_equals("Status.mData",  py.mPy->getStatus().mData,  2); +    } + +    template<> template<> +    void object::test<6>() +    { +        set_test_name("syntax_error:"); +        PythonProcessLauncher py(get_test_name(), +                                 "syntax_error:\n"); +        py.mParams.files.add(LLProcess::FileParam()); // inherit stdin +        py.mParams.files.add(LLProcess::FileParam()); // inherit stdout +        py.mParams.files.add(LLProcess::FileParam().type("pipe")); // pipe for stderr +        py.run(); +        ensure_equals("Status.mState", py.mPy->getStatus().mState, LLProcess::EXITED); +        ensure_equals("Status.mData",  py.mPy->getStatus().mData,  1); +        std::istream& rpipe(py.mPy->getReadPipe(LLProcess::STDERR).get_istream()); +        std::vector<char> buffer(4096); +        rpipe.read(&buffer[0], buffer.size()); +        std::streamsize got(rpipe.gcount()); +        ensure("Nothing read from stderr pipe", got); +        std::string data(&buffer[0], got); +        ensure("Didn't find 'SyntaxError:'", data.find("\nSyntaxError:") != std::string::npos); +    } + +    template<> template<> +    void object::test<7>() +    { +        set_test_name("explicit kill()"); +        PythonProcessLauncher py(get_test_name(), +                                 "from __future__ import with_statement\n" +                                 "import sys, time\n" +                                 "with open(sys.argv[1], 'w') as f:\n" +                                 "    f.write('ok')\n" +                                 "# now sleep; expect caller to kill\n" +                                 "time.sleep(120)\n" +                                 "# if caller hasn't managed to kill by now, bad\n" +                                 "with open(sys.argv[1], 'w') as f:\n" +                                 "    f.write('bad')\n"); +        NamedTempFile out("out", "not started"); +        py.mParams.args.add(out.getName()); +        py.launch(); +        // Wait for the script to wake up and do its first write +        int i = 0, timeout = 60; +        for ( ; i < timeout; ++i) +        { +            yield(); +            if (readfile(out.getName(), "from kill() script") == "ok") +                break; +        } +        // If we broke this loop because of the counter, something's wrong +        ensure("script never started", i < timeout); +        // script has performed its first write and should now be sleeping. +        py.mPy->kill(); +        // wait for the script to terminate... one way or another. +        waitfor(*py.mPy); +#if LL_WINDOWS +        ensure_equals("Status.mState", py.mPy->getStatus().mState, LLProcess::EXITED); +        ensure_equals("Status.mData",  py.mPy->getStatus().mData,  -1); +#else +        ensure_equals("Status.mState", py.mPy->getStatus().mState, LLProcess::KILLED); +        ensure_equals("Status.mData",  py.mPy->getStatus().mData,  SIGTERM); +#endif +        // If kill() failed, the script would have woken up on its own and +        // overwritten the file with 'bad'. But if kill() succeeded, it should +        // not have had that chance. +        ensure_equals(get_test_name() + " script output", readfile(out.getName()), "ok"); +    } + +    template<> template<> +    void object::test<8>() +    { +        set_test_name("implicit kill()"); +        NamedTempFile out("out", "not started"); +        LLProcess::handle phandle(0); +        { +            PythonProcessLauncher py(get_test_name(), +                                     "from __future__ import with_statement\n" +                                     "import sys, time\n" +                                     "with open(sys.argv[1], 'w') as f:\n" +                                     "    f.write('ok')\n" +                                     "# now sleep; expect caller to kill\n" +                                     "time.sleep(120)\n" +                                     "# if caller hasn't managed to kill by now, bad\n" +                                     "with open(sys.argv[1], 'w') as f:\n" +                                     "    f.write('bad')\n"); +            py.mParams.args.add(out.getName()); +            py.launch(); +            // Capture handle for later +            phandle = py.mPy->getProcessHandle(); +            // Wait for the script to wake up and do its first write +            int i = 0, timeout = 60; +            for ( ; i < timeout; ++i) +            { +                yield(); +                if (readfile(out.getName(), "from kill() script") == "ok") +                    break; +            } +            // If we broke this loop because of the counter, something's wrong +            ensure("script never started", i < timeout); +            // Script has performed its first write and should now be sleeping. +            // Destroy the LLProcess, which should kill the child. +        } +        // wait for the script to terminate... one way or another. +        waitfor(phandle, "kill() script"); +        // If kill() failed, the script would have woken up on its own and +        // overwritten the file with 'bad'. But if kill() succeeded, it should +        // not have had that chance. +        ensure_equals(get_test_name() + " script output", readfile(out.getName()), "ok"); +    } + +    template<> template<> +    void object::test<9>() +    { +        set_test_name("autokill=false"); +        NamedTempFile from("from", "not started"); +        NamedTempFile to("to", ""); +        LLProcess::handle phandle(0); +        { +            PythonProcessLauncher py(get_test_name(), +                                     "from __future__ import with_statement\n" +                                     "import sys, time\n" +                                     "with open(sys.argv[1], 'w') as f:\n" +                                     "    f.write('ok')\n" +                                     "# wait for 'go' from test program\n" +                                     "for i in xrange(60):\n" +                                     "    time.sleep(1)\n" +                                     "    with open(sys.argv[2]) as f:\n" +                                     "        go = f.read()\n" +                                     "    if go == 'go':\n" +                                     "        break\n" +                                     "else:\n" +                                     "    with open(sys.argv[1], 'w') as f:\n" +                                     "        f.write('never saw go')\n" +                                     "    sys.exit(1)\n" +                                     "# okay, saw 'go', write 'ack'\n" +                                     "with open(sys.argv[1], 'w') as f:\n" +                                     "    f.write('ack')\n"); +            py.mParams.args.add(from.getName()); +            py.mParams.args.add(to.getName()); +            py.mParams.autokill = false; +            py.launch(); +            // Capture handle for later +            phandle = py.mPy->getProcessHandle(); +            // Wait for the script to wake up and do its first write +            int i = 0, timeout = 60; +            for ( ; i < timeout; ++i) +            { +                yield(); +                if (readfile(from.getName(), "from autokill script") == "ok") +                    break; +            } +            // If we broke this loop because of the counter, something's wrong +            ensure("script never started", i < timeout); +            // Now destroy the LLProcess, which should NOT kill the child! +        } +        // If the destructor killed the child anyway, give it time to die +        yield(2); +        // How do we know it's not terminated? By making it respond to +        // a specific stimulus in a specific way. +        { +            std::ofstream outf(to.getName().c_str()); +            outf << "go"; +        } // flush and close. +        // now wait for the script to terminate... one way or another. +        waitfor(phandle, "autokill script"); +        // If the LLProcess destructor implicitly called kill(), the +        // script could not have written 'ack' as we expect. +        ensure_equals(get_test_name() + " script output", readfile(from.getName()), "ack"); +    } + +    template<> template<> +    void object::test<10>() +    { +        set_test_name("'bogus' test"); +        CaptureLog recorder; +        PythonProcessLauncher py(get_test_name(), +                                 "print 'Hello world'\n"); +        py.mParams.files.add(LLProcess::FileParam("bogus")); +        py.mPy = LLProcess::create(py.mParams); +        ensure("should have rejected 'bogus'", ! py.mPy); +        std::string message(recorder.messageWith("bogus")); +        ensure_contains("did not name 'stdin'", message, "stdin"); +    } + +    template<> template<> +    void object::test<11>() +    { +        set_test_name("'file' test"); +        // Replace this test with one or more real 'file' tests when we +        // implement 'file' support +        PythonProcessLauncher py(get_test_name(), +                                 "print 'Hello world'\n"); +        py.mParams.files.add(LLProcess::FileParam()); +        py.mParams.files.add(LLProcess::FileParam("file")); +        py.mPy = LLProcess::create(py.mParams); +        ensure("should have rejected 'file'", ! py.mPy); +    } + +    template<> template<> +    void object::test<12>() +    { +        set_test_name("'tpipe' test"); +        // Replace this test with one or more real 'tpipe' tests when we +        // implement 'tpipe' support +        CaptureLog recorder; +        PythonProcessLauncher py(get_test_name(), +                                 "print 'Hello world'\n"); +        py.mParams.files.add(LLProcess::FileParam()); +        py.mParams.files.add(LLProcess::FileParam("tpipe")); +        py.mPy = LLProcess::create(py.mParams); +        ensure("should have rejected 'tpipe'", ! py.mPy); +        std::string message(recorder.messageWith("tpipe")); +        ensure_contains("did not name 'stdout'", message, "stdout"); +    } + +    template<> template<> +    void object::test<13>() +    { +        set_test_name("'npipe' test"); +        // Replace this test with one or more real 'npipe' tests when we +        // implement 'npipe' support +        CaptureLog recorder; +        PythonProcessLauncher py(get_test_name(), +                                 "print 'Hello world'\n"); +        py.mParams.files.add(LLProcess::FileParam()); +        py.mParams.files.add(LLProcess::FileParam()); +        py.mParams.files.add(LLProcess::FileParam("npipe")); +        py.mPy = LLProcess::create(py.mParams); +        ensure("should have rejected 'npipe'", ! py.mPy); +        std::string message(recorder.messageWith("npipe")); +        ensure_contains("did not name 'stderr'", message, "stderr"); +    } + +    template<> template<> +    void object::test<14>() +    { +        set_test_name("internal pipe name warning"); +        CaptureLog recorder; +        PythonProcessLauncher py(get_test_name(), +                                 "import sys\n" +                                 "sys.exit(7)\n"); +        py.mParams.files.add(LLProcess::FileParam("pipe", "somename")); +        py.run();                   // verify that it did launch anyway +        ensure_equals("Status.mState", py.mPy->getStatus().mState, LLProcess::EXITED); +        ensure_equals("Status.mData",  py.mPy->getStatus().mData,  7); +        std::string message(recorder.messageWith("not yet supported")); +        ensure_contains("log message did not mention internal pipe name", +                        message, "somename"); +    } + +    /*-------------- support for "get*Pipe() validation" test --------------*/ +#define TEST_getPipe(PROCESS, GETPIPE, GETOPTPIPE, VALID, NOPIPE, BADPIPE) \ +    do                                                                  \ +    {                                                                   \ +        std::string threw;                                              \ +        /* Both the following calls should work. */                     \ +        (PROCESS).GETPIPE(VALID);                                       \ +        ensure(#GETOPTPIPE "(" #VALID ") failed", (PROCESS).GETOPTPIPE(VALID)); \ +        /* pass obviously bogus PIPESLOT */                             \ +        CATCH_IN(threw, LLProcess::NoPipe, (PROCESS).GETPIPE(LLProcess::FILESLOT(4))); \ +        ensure_contains("didn't reject bad slot", threw, "no slot");    \ +        ensure_contains("didn't mention bad slot num", threw, "4");     \ +        EXPECT_FAIL_WITH_LOG(threw, (PROCESS).GETOPTPIPE(LLProcess::FILESLOT(4))); \ +        /* pass NOPIPE */                                               \ +        CATCH_IN(threw, LLProcess::NoPipe, (PROCESS).GETPIPE(NOPIPE));  \ +        ensure_contains("didn't reject non-pipe", threw, "not a monitored"); \ +        EXPECT_FAIL_WITH_LOG(threw, (PROCESS).GETOPTPIPE(NOPIPE));      \ +        /* pass BADPIPE: FILESLOT isn't empty but wrong direction */    \ +        CATCH_IN(threw, LLProcess::NoPipe, (PROCESS).GETPIPE(BADPIPE)); \ +        /* sneaky: GETPIPE is getReadPipe or getWritePipe */            \ +        /* so skip "get" to obtain ReadPipe or WritePipe  :-P  */       \ +        ensure_contains("didn't reject wrong pipe", threw, (#GETPIPE)+3); \ +        EXPECT_FAIL_WITH_LOG(threw, (PROCESS).GETOPTPIPE(BADPIPE));     \ +    } while (0) + +/// For expecting exceptions. Execute CODE, catch EXCEPTION, store its what() +/// in std::string THREW, ensure it's not empty (i.e. EXCEPTION did happen). +#define CATCH_IN(THREW, EXCEPTION, CODE)                                \ +    do                                                                  \ +    {                                                                   \ +        (THREW).clear();                                                \ +        try                                                             \ +        {                                                               \ +            CODE;                                                       \ +        }                                                               \ +        CATCH_AND_STORE_WHAT_IN(THREW, EXCEPTION)                       \ +        ensure("failed to throw " #EXCEPTION ": " #CODE, ! (THREW).empty()); \ +    } while (0) + +#define EXPECT_FAIL_WITH_LOG(EXPECT, CODE)                              \ +    do                                                                  \ +    {                                                                   \ +        CaptureLog recorder;                                            \ +        ensure(#CODE " succeeded", ! (CODE));                           \ +        recorder.messageWith(EXPECT);                                   \ +    } while (0) + +    template<> template<> +    void object::test<15>() +    { +        set_test_name("get*Pipe() validation"); +        PythonProcessLauncher py(get_test_name(), +                                 "print 'this output is expected'\n"); +        py.mParams.files.add(LLProcess::FileParam("pipe")); // pipe for  stdin +        py.mParams.files.add(LLProcess::FileParam());       // inherit stdout +        py.mParams.files.add(LLProcess::FileParam("pipe")); // pipe for stderr +        py.run(); +        TEST_getPipe(*py.mPy, getWritePipe, getOptWritePipe, +                     LLProcess::STDIN,   // VALID +                     LLProcess::STDOUT,  // NOPIPE +                     LLProcess::STDERR); // BADPIPE +        TEST_getPipe(*py.mPy, getReadPipe,  getOptReadPipe, +                     LLProcess::STDERR,  // VALID +                     LLProcess::STDOUT,  // NOPIPE +                     LLProcess::STDIN);  // BADPIPE +    } + +    template<> template<> +    void object::test<16>() +    { +        set_test_name("talk to stdin/stdout"); +        PythonProcessLauncher py(get_test_name(), +                                 "import sys, time\n" +                                 "print 'ok'\n" +                                 "sys.stdout.flush()\n" +                                 "# wait for 'go' from test program\n" +                                 "go = sys.stdin.readline()\n" +                                 "if go != 'go\\n':\n" +                                 "    sys.exit('expected \"go\", saw %r' % go)\n" +                                 "print 'ack'\n"); +        py.mParams.files.add(LLProcess::FileParam("pipe")); // stdin +        py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout +        py.launch(); +        LLProcess::ReadPipe& childout(py.mPy->getReadPipe(LLProcess::STDOUT)); +        int i, timeout = 60; +        for (i = 0; i < timeout && py.mPy->isRunning() && childout.size() < 3; ++i) +        { +            yield(); +        } +        ensure("script never started", i < timeout); +        ensure_equals("bad wakeup from stdin/stdout script", +                      childout.getline(), "ok"); +        // important to get the implicit flush from std::endl +        py.mPy->getWritePipe().get_ostream() << "go" << std::endl; +        for (i = 0; i < timeout && py.mPy->isRunning() && ! childout.contains("\n"); ++i) +        { +            yield(); +        } +        ensure("script never replied", childout.contains("\n")); +        ensure_equals("child didn't ack", childout.getline(), "ack"); +        ensure_equals("bad child termination", py.mPy->getStatus().mState, LLProcess::EXITED); +        ensure_equals("bad child exit code",   py.mPy->getStatus().mData,  0); +    } + +    struct EventListener: public boost::noncopyable +    { +        EventListener(LLEventPump& pump) +        { +            mConnection =  +                pump.listen("EventListener", boost::bind(&EventListener::tick, this, _1)); +        } + +        bool tick(const LLSD& data) +        { +            mHistory.push_back(data); +            return false; +        } + +        std::list<LLSD> mHistory; +        LLTempBoundListener mConnection; +    }; + +    static bool ack(std::ostream& out, const LLSD& data) +    { +        out << "continue" << std::endl; +        return false; +    } + +    template<> template<> +    void object::test<17>() +    { +        set_test_name("listen for ReadPipe events"); +        PythonProcessLauncher py(get_test_name(), +                                 "import sys\n" +                                 "sys.stdout.write('abc')\n" +                                 "sys.stdout.flush()\n" +                                 "sys.stdin.readline()\n" +                                 "sys.stdout.write('def')\n" +                                 "sys.stdout.flush()\n" +                                 "sys.stdin.readline()\n" +                                 "sys.stdout.write('ghi\\n')\n" +                                 "sys.stdout.flush()\n" +                                 "sys.stdin.readline()\n" +                                 "sys.stdout.write('second line\\n')\n"); +        py.mParams.files.add(LLProcess::FileParam("pipe")); // stdin +        py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout +        py.launch(); +        std::ostream& childin(py.mPy->getWritePipe(LLProcess::STDIN).get_ostream()); +        LLProcess::ReadPipe& childout(py.mPy->getReadPipe(LLProcess::STDOUT)); +        // lift the default limit; allow event to carry (some of) the actual data +        childout.setLimit(20); +        // listen for incoming data on childout +        EventListener listener(childout.getPump()); +        // also listen with a function that prompts the child to continue +        // every time we see output +        LLTempBoundListener connection( +            childout.getPump().listen("ack", boost::bind(ack, boost::ref(childin), _1))); +        int i, timeout = 60; +        // wait through stuttering first line +        for (i = 0; i < timeout && py.mPy->isRunning() && ! childout.contains("\n"); ++i) +        { +            yield(); +        } +        ensure("couldn't get first line", i < timeout); +        // disconnect from listener +        listener.mConnection.disconnect(); +        // finish out the run +        waitfor(*py.mPy); +        // now verify history +        std::list<LLSD>::const_iterator li(listener.mHistory.begin()), +                                        lend(listener.mHistory.end()); +        ensure("no events", li != lend); +        ensure_equals("history[0]", (*li)["data"].asString(), "abc"); +        ensure_equals("history[0] len", (*li)["len"].asInteger(), 3); +        ++li; +        ensure("only 1 event", li != lend); +        ensure_equals("history[1]", (*li)["data"].asString(), "abcdef"); +        ensure_equals("history[0] len", (*li)["len"].asInteger(), 6); +        ++li; +        ensure("only 2 events", li != lend); +        ensure_equals("history[2]", (*li)["data"].asString(), "abcdefghi" EOL); +        ensure_equals("history[0] len", (*li)["len"].asInteger(), 9 + sizeof(EOL) - 1); +        ++li; +        // We DO NOT expect a whole new event for the second line because we +        // disconnected. +        ensure("more than 3 events", li == lend); +    } + +    template<> template<> +    void object::test<18>() +    { +        set_test_name("ReadPipe \"eof\" event"); +        PythonProcessLauncher py(get_test_name(), +                                 "print 'Hello from Python!'\n"); +        py.mParams.files.add(LLProcess::FileParam()); // stdin +        py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout +        py.launch(); +        LLProcess::ReadPipe& childout(py.mPy->getReadPipe(LLProcess::STDOUT)); +        EventListener listener(childout.getPump()); +        waitfor(*py.mPy); +        // We can't be positive there will only be a single event, if the OS +        // (or any other intervening layer) does crazy buffering. What we want +        // to ensure is that there was exactly ONE event with "eof" true, and +        // that it was the LAST event. +        std::list<LLSD>::const_reverse_iterator rli(listener.mHistory.rbegin()), +                                                rlend(listener.mHistory.rend()); +        ensure("no events", rli != rlend); +        ensure("last event not \"eof\"", (*rli)["eof"].asBoolean()); +        while (++rli != rlend) +        { +            ensure("\"eof\" event not last", ! (*rli)["eof"].asBoolean()); +        } +    } + +    template<> template<> +    void object::test<19>() +    { +        set_test_name("setLimit()"); +        PythonProcessLauncher py(get_test_name(), +                                 "import sys\n" +                                 "sys.stdout.write(sys.argv[1])\n"); +        std::string abc("abcdefghijklmnopqrstuvwxyz"); +        py.mParams.args.add(abc); +        py.mParams.files.add(LLProcess::FileParam()); // stdin +        py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout +        py.launch(); +        LLProcess::ReadPipe& childout(py.mPy->getReadPipe(LLProcess::STDOUT)); +        // listen for incoming data on childout +        EventListener listener(childout.getPump()); +        // but set limit +        childout.setLimit(10); +        ensure_equals("getLimit() after setlimit(10)", childout.getLimit(), 10); +        // okay, pump I/O to pick up output from child +        waitfor(*py.mPy); +        ensure("no events", ! listener.mHistory.empty()); +        // For all we know, that data could have arrived in several different +        // bursts... probably not, but anyway, only check the last one. +        ensure_equals("event[\"len\"]", +                      listener.mHistory.back()["len"].asInteger(), abc.length()); +        ensure_equals("length of setLimit(10) data", +                      listener.mHistory.back()["data"].asString().length(), 10); +    } + +    template<> template<> +    void object::test<20>() +    { +        set_test_name("peek() ReadPipe data"); +        PythonProcessLauncher py(get_test_name(), +                                 "import sys\n" +                                 "sys.stdout.write(sys.argv[1])\n"); +        std::string abc("abcdefghijklmnopqrstuvwxyz"); +        py.mParams.args.add(abc); +        py.mParams.files.add(LLProcess::FileParam()); // stdin +        py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout +        py.launch(); +        LLProcess::ReadPipe& childout(py.mPy->getReadPipe(LLProcess::STDOUT)); +        // okay, pump I/O to pick up output from child +        waitfor(*py.mPy); +        // peek() with substr args +        ensure_equals("peek()", childout.peek(), abc); +        ensure_equals("peek(23)", childout.peek(23), abc.substr(23)); +        ensure_equals("peek(5, 3)", childout.peek(5, 3), abc.substr(5, 3)); +        ensure_equals("peek(27, 2)", childout.peek(27, 2), ""); +        ensure_equals("peek(23, 5)", childout.peek(23, 5), "xyz"); +        // contains() -- we don't exercise as thoroughly as find() because the +        // contains() implementation is trivially (and visibly) based on find() +        ensure("contains(\":\")", ! childout.contains(":")); +        ensure("contains(':')",   ! childout.contains(':')); +        ensure("contains(\"d\")", childout.contains("d")); +        ensure("contains('d')",   childout.contains('d')); +        ensure("contains(\"klm\")", childout.contains("klm")); +        ensure("contains(\"klx\")", ! childout.contains("klx")); +        // find() +        ensure("find(\":\")", childout.find(":") == LLProcess::ReadPipe::npos); +        ensure("find(':')",   childout.find(':') == LLProcess::ReadPipe::npos); +        ensure_equals("find(\"d\")", childout.find("d"), 3); +        ensure_equals("find('d')",   childout.find('d'), 3); +        ensure_equals("find(\"d\", 3)", childout.find("d", 3), 3); +        ensure_equals("find('d', 3)",   childout.find('d', 3), 3); +        ensure("find(\"d\", 4)", childout.find("d", 4) == LLProcess::ReadPipe::npos); +        ensure("find('d', 4)",   childout.find('d', 4) == LLProcess::ReadPipe::npos); +        // The case of offset == end and offset > end are different. In the +        // first case, we can form a valid (albeit empty) iterator range and +        // search that. In the second, guard logic in the implementation must +        // realize we can't form a valid iterator range. +        ensure("find(\"d\", 26)", childout.find("d", 26) == LLProcess::ReadPipe::npos); +        ensure("find('d', 26)",   childout.find('d', 26) == LLProcess::ReadPipe::npos); +        ensure("find(\"d\", 27)", childout.find("d", 27) == LLProcess::ReadPipe::npos); +        ensure("find('d', 27)",   childout.find('d', 27) == LLProcess::ReadPipe::npos); +        ensure_equals("find(\"ghi\")", childout.find("ghi"), 6); +        ensure_equals("find(\"ghi\", 6)", childout.find("ghi"), 6); +        ensure("find(\"ghi\", 7)", childout.find("ghi", 7) == LLProcess::ReadPipe::npos); +        ensure("find(\"ghi\", 26)", childout.find("ghi", 26) == LLProcess::ReadPipe::npos); +        ensure("find(\"ghi\", 27)", childout.find("ghi", 27) == LLProcess::ReadPipe::npos); +    } + +    template<> template<> +    void object::test<21>() +    { +        set_test_name("bad postend"); +        std::string pumpname("postend"); +        EventListener listener(LLEventPumps::instance().obtain(pumpname)); +        LLProcess::Params params; +        params.desc = get_test_name(); +        params.postend = pumpname; +        LLProcessPtr child = LLProcess::create(params); +        ensure("shouldn't have launched", ! child); +        ensure_equals("number of postend events", listener.mHistory.size(), 1); +        LLSD postend(listener.mHistory.front()); +        ensure("has id", ! postend.has("id")); +        ensure_equals("desc", postend["desc"].asString(), std::string(params.desc)); +        ensure_equals("state", postend["state"].asInteger(), LLProcess::UNSTARTED); +        ensure("has data", ! postend.has("data")); +        std::string error(postend["string"]); +        // All we get from canned parameter validation is a bool, so the +        // "validation failed" message we ourselves generate can't mention +        // "executable" by name. Just check that it's nonempty. +        //ensure_contains("error", error, "executable"); +        ensure("string", ! error.empty()); +    } + +    template<> template<> +    void object::test<22>() +    { +        set_test_name("good postend"); +        PythonProcessLauncher py(get_test_name(), +                                 "import sys\n" +                                 "sys.exit(35)\n"); +        std::string pumpname("postend"); +        EventListener listener(LLEventPumps::instance().obtain(pumpname)); +        py.mParams.postend = pumpname; +        py.launch(); +        LLProcess::id childid(py.mPy->getProcessID()); +        // Don't use waitfor(), which calls isRunning(); instead wait for an +        // event on pumpname. +        int i, timeout = 60; +        for (i = 0; i < timeout && listener.mHistory.empty(); ++i) +        { +            yield(); +        } +        ensure("no postend event", i < timeout); +        ensure_equals("number of postend events", listener.mHistory.size(), 1); +        LLSD postend(listener.mHistory.front()); +        ensure_equals("id",    postend["id"].asInteger(), childid); +        ensure("desc empty", ! postend["desc"].asString().empty()); +        ensure_equals("state", postend["state"].asInteger(), LLProcess::EXITED); +        ensure_equals("data",  postend["data"].asInteger(),  35); +        std::string str(postend["string"]); +        ensure_contains("string", str, "exited"); +        ensure_contains("string", str, "35"); +    } + +    struct PostendListener +    { +        PostendListener(LLProcess::ReadPipe& rpipe, +                        const std::string& pumpname, +                        const std::string& expect): +            mReadPipe(rpipe), +            mExpect(expect), +            mTriggered(false) +        { +            LLEventPumps::instance().obtain(pumpname) +                .listen("PostendListener", boost::bind(&PostendListener::postend, this, _1)); +        } + +        bool postend(const LLSD&) +        { +            mTriggered = true; +            ensure_equals("postend listener", mReadPipe.read(mReadPipe.size()), mExpect); +            return false; +        } + +        LLProcess::ReadPipe& mReadPipe; +        std::string mExpect; +        bool mTriggered; +    }; + +    template<> template<> +    void object::test<23>() +    { +        set_test_name("all data visible at postend"); +        PythonProcessLauncher py(get_test_name(), +                                 "import sys\n" +                                 // note, no '\n' in written data +                                 "sys.stdout.write('partial line')\n"); +        std::string pumpname("postend"); +        py.mParams.files.add(LLProcess::FileParam()); // stdin +        py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout +        py.mParams.postend = pumpname; +        py.launch(); +        PostendListener listener(py.mPy->getReadPipe(LLProcess::STDOUT), +                                 pumpname, +                                 "partial line"); +        waitfor(*py.mPy); +        ensure("postend never triggered", listener.mTriggered); +    } +} // namespace tut diff --git a/indra/llcommon/tests/llsdserialize_test.cpp b/indra/llcommon/tests/llsdserialize_test.cpp index 72322c3b72..e625545763 100644 --- a/indra/llcommon/tests/llsdserialize_test.cpp +++ b/indra/llcommon/tests/llsdserialize_test.cpp @@ -40,41 +40,15 @@ typedef U32 uint32_t;  #include <fcntl.h>  #include <sys/stat.h>  #include <sys/wait.h> -#include "llprocesslauncher.h" +#include "llprocess.h"  #endif -#include <sstream> - -/*==========================================================================*| -// Whoops, seems Linden's Boost package and the viewer are built with -// different settings of VC's /Zc:wchar_t switch! Using Boost.Filesystem -// pathname operations produces Windows link errors: -// unresolved external symbol "private: static class std::codecvt<unsigned short, -// char,int> const * & __cdecl boost::filesystem3::path::wchar_t_codecvt_facet()" -// unresolved external symbol "void __cdecl boost::filesystem3::path_traits::convert()" -// See: -// http://boost.2283326.n4.nabble.com/filesystem-v3-unicode-and-std-codecvt-linker-error-td3455549.html -// which points to: -// http://msdn.microsoft.com/en-us/library/dh8che7s%28v=VS.100%29.aspx - -// As we're not trying to preserve compatibility with old Boost.Filesystem -// code, but rather writing brand-new code, use the newest available -// Filesystem API. -#define BOOST_FILESYSTEM_VERSION 3 -#include "boost/filesystem.hpp" -#include "boost/filesystem/v3/fstream.hpp" -|*==========================================================================*/  #include "boost/range.hpp"  #include "boost/foreach.hpp"  #include "boost/function.hpp"  #include "boost/lambda/lambda.hpp"  #include "boost/lambda/bind.hpp"  namespace lambda = boost::lambda; -/*==========================================================================*| -// Aaaarrgh, Linden's Boost package doesn't even include Boost.Iostreams! -#include "boost/iostreams/stream.hpp" -#include "boost/iostreams/device/file_descriptor.hpp" -|*==========================================================================*/  #include "../llsd.h"  #include "../llsdserialize.h" @@ -82,236 +56,17 @@ namespace lambda = boost::lambda;  #include "../llformat.h"  #include "../test/lltut.h" +#include "../test/manageapr.h" +#include "../test/namedtempfile.h"  #include "stringize.h" +static ManageAPR manager; +  std::vector<U8> string_to_vector(const std::string& str)  {  	return std::vector<U8>(str.begin(), str.end());  } -#if ! LL_WINDOWS -// We want to call strerror_r(), but alarmingly, there are two different -// variants. The one that returns int always populates the passed buffer -// (except in case of error), whereas the other one always returns a valid -// char* but might or might not populate the passed buffer. How do we know -// which one we're getting? Define adapters for each and let the compiler -// select the applicable adapter. - -// strerror_r() returns char* -std::string message_from(int /*orig_errno*/, const char* /*buffer*/, const char* strerror_ret) -{ -    return strerror_ret; -} - -// strerror_r() returns int -std::string message_from(int orig_errno, const char* buffer, int strerror_ret) -{ -    if (strerror_ret == 0) -    { -        return buffer; -    } -    // Here strerror_r() has set errno. Since strerror_r() has already failed, -    // seems like a poor bet to call it again to diagnose its own error... -    int stre_errno = errno; -    if (stre_errno == ERANGE) -    { -        return STRINGIZE("strerror_r() can't explain errno " << orig_errno -                         << " (buffer too small)"); -    } -    if (stre_errno == EINVAL) -    { -        return STRINGIZE("unknown errno " << orig_errno); -    } -    // Here we don't even understand the errno from strerror_r()! -    return STRINGIZE("strerror_r() can't explain errno " << orig_errno -                     << " (error " << stre_errno << ')'); -} -#endif  // ! LL_WINDOWS - -// boost::filesystem::temp_directory_path() isn't yet in Boost 1.45! :-( -std::string temp_directory_path() -{ -#if LL_WINDOWS -    char buffer[4096]; -    GetTempPathA(sizeof(buffer), buffer); -    return buffer; - -#else  // LL_DARWIN, LL_LINUX -    static const char* vars[] = { "TMPDIR", "TMP", "TEMP", "TEMPDIR" }; -    BOOST_FOREACH(const char* var, vars) -    { -        const char* found = getenv(var); -        if (found) -            return found; -    } -    return "/tmp"; -#endif // LL_DARWIN, LL_LINUX -} - -// Windows presents a kinda sorta compatibility layer. Code to the yucky -// Windows names because they're less likely than the Posix names to collide -// with any other names in this source. -#if LL_WINDOWS -#define _remove   DeleteFileA -#else  // ! LL_WINDOWS -#define _open     open -#define _write    write -#define _close    close -#define _remove   remove -#endif  // ! LL_WINDOWS - -// Create a text file with specified content "somewhere in the -// filesystem," cleaning up when it goes out of scope. -class NamedTempFile -{ -public: -    // Function that accepts an ostream ref and (presumably) writes stuff to -    // it, e.g.: -    // (lambda::_1 << "the value is " << 17 << '\n') -    typedef boost::function<void(std::ostream&)> Streamer; - -    NamedTempFile(const std::string& ext, const std::string& content): -        mPath(temp_directory_path()) -    { -        createFile(ext, lambda::_1 << content); -    } - -    // Disambiguate when passing string literal -    NamedTempFile(const std::string& ext, const char* content): -        mPath(temp_directory_path()) -    { -        createFile(ext, lambda::_1 << content); -    } - -    NamedTempFile(const std::string& ext, const Streamer& func): -        mPath(temp_directory_path()) -    { -        createFile(ext, func); -    } - -    ~NamedTempFile() -    { -        _remove(mPath.c_str()); -    } - -    std::string getName() const { return mPath; } - -private: -    void createFile(const std::string& ext, const Streamer& func) -    { -        // Silly maybe, but use 'ext' as the name prefix. Strip off a leading -        // '.' if present. -        int pfx_offset = ((! ext.empty()) && ext[0] == '.')? 1 : 0; - -#if ! LL_WINDOWS -        // Make sure mPath ends with a directory separator, if it doesn't already. -        if (mPath.empty() || -            ! (mPath[mPath.length() - 1] == '\\' || mPath[mPath.length() - 1] == '/')) -        { -            mPath.append("/"); -        } - -        // mkstemp() accepts and modifies a char* template string. Generate -        // the template string, then copy to modifiable storage. -        // mkstemp() requires its template string to end in six X's. -        mPath += ext.substr(pfx_offset) + "XXXXXX"; -        // Copy to vector<char> -        std::vector<char> pathtemplate(mPath.begin(), mPath.end()); -        // append a nul byte for classic-C semantics -        pathtemplate.push_back('\0'); -        // std::vector promises that a pointer to the 0th element is the same -        // as a pointer to a contiguous classic-C array -        int fd(mkstemp(&pathtemplate[0])); -        if (fd == -1) -        { -            // The documented errno values (http://linux.die.net/man/3/mkstemp) -            // are used in a somewhat unusual way, so provide context-specific -            // errors. -            if (errno == EEXIST) -            { -                LL_ERRS("NamedTempFile") << "mkstemp(\"" << mPath -                                         << "\") could not create unique file " << LL_ENDL; -            } -            if (errno == EINVAL) -            { -                LL_ERRS("NamedTempFile") << "bad mkstemp() file path template '" -                                         << mPath << "'" << LL_ENDL; -            } -            // Shrug, something else -            int mkst_errno = errno; -            char buffer[256]; -            LL_ERRS("NamedTempFile") << "mkstemp(\"" << mPath << "\") failed: " -                                     << message_from(mkst_errno, buffer, -                                                     strerror_r(mkst_errno, buffer, sizeof(buffer))) -                                     << LL_ENDL; -        } -        // mkstemp() seems to have worked! Capture the modified filename. -        // Avoid the nul byte we appended. -        mPath.assign(pathtemplate.begin(), (pathtemplate.end()-1)); - -/*==========================================================================*| -        // Define an ostream on the open fd. Tell it to close fd on destruction. -        boost::iostreams::stream<boost::iostreams::file_descriptor_sink> -            out(fd, boost::iostreams::close_handle); -|*==========================================================================*/ - -        // Write desired content. -        std::ostringstream out; -        // Stream stuff to it. -        func(out); - -        std::string data(out.str()); -        int written(_write(fd, data.c_str(), data.length())); -        int closed(_close(fd)); -        llassert_always(written == data.length() && closed == 0); - -#else // LL_WINDOWS -        // GetTempFileName() is documented to require a MAX_PATH buffer. -        char tempname[MAX_PATH]; -        // Use 'ext' as filename prefix, but skip leading '.' if any. -        // The 0 param is very important: requests iterating until we get a -        // unique name. -        if (0 == GetTempFileNameA(mPath.c_str(), ext.c_str() + pfx_offset, 0, tempname)) -        { -            // I always have to look up this call...  :-P -            LPSTR msgptr; -            FormatMessageA( -                FORMAT_MESSAGE_ALLOCATE_BUFFER |  -                FORMAT_MESSAGE_FROM_SYSTEM | -                FORMAT_MESSAGE_IGNORE_INSERTS, -                NULL, -                GetLastError(), -                MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), -                LPSTR(&msgptr),     // have to cast (char**) to (char*) -                0, NULL ); -            LL_ERRS("NamedTempFile") << "GetTempFileName(\"" << mPath << "\", \"" -                                     << (ext.c_str() + pfx_offset) << "\") failed: " -                                     << msgptr << LL_ENDL; -            LocalFree(msgptr); -        } -        // GetTempFileName() appears to have worked! Capture the actual -        // filename. -        mPath = tempname; -        // Open the file and stream content to it. Destructor will close. -        std::ofstream out(tempname); -        func(out); - -#endif  // LL_WINDOWS -    } - -    void peep() -    { -        std::cout << "File '" << mPath << "' contains:\n"; -        std::ifstream reader(mPath.c_str()); -        std::string line; -        while (std::getline(reader, line)) -            std::cout << line << '\n'; -        std::cout << "---\n"; -    } - -    std::string mPath; -}; -  namespace tut  {  	struct sd_xml_data @@ -1783,7 +1538,7 @@ namespace tut              const char* PYTHON(getenv("PYTHON"));              ensure("Set $PYTHON to the Python interpreter", PYTHON); -            NamedTempFile scriptfile(".py", script); +            NamedTempFile scriptfile("py", script);  #if LL_WINDOWS              std::string q("\""); @@ -1802,14 +1557,15 @@ namespace tut              }  #else  // LL_DARWIN, LL_LINUX -            LLProcessLauncher py; -            py.setExecutable(PYTHON); -            py.addArgument(scriptfile.getName()); -            ensure_equals(STRINGIZE("Couldn't launch " << desc << " script"), py.launch(), 0); +            LLProcess::Params params; +            params.executable = PYTHON; +            params.args.add(scriptfile.getName()); +            LLProcessPtr py(LLProcess::create(params)); +            ensure(STRINGIZE("Couldn't launch " << desc << " script"), py);              // Implementing timeout would mean messing with alarm() and              // catching SIGALRM... later maybe...              int status(0); -            if (waitpid(py.getProcessID(), &status, 0) == -1) +            if (waitpid(py->getProcessID(), &status, 0) == -1)              {                  int waitpid_errno(errno);                  ensure_equals(STRINGIZE("Couldn't retrieve rc from " << desc << " script: " @@ -1888,12 +1644,12 @@ namespace tut              "    else:\n"              "        assert False, 'Too many data items'\n"; -        // Create a something.llsd file containing 'data' serialized to +        // Create an llsdXXXXXX file containing 'data' serialized to          // notation. It's important to separate with newlines because Python's          // llsd module doesn't support parsing from a file stream, only from a          // string, so we have to know how much of the file to read into a          // string. -        NamedTempFile file(".llsd", +        NamedTempFile file("llsd",                             // NamedTempFile's boost::function constructor                             // takes a callable. To this callable it passes the                             // std::ostream with which it's writing the @@ -1926,7 +1682,7 @@ namespace tut          // Create an empty data file. This is just a placeholder for our          // script to write into. Create it to establish a unique name that          // we know. -        NamedTempFile file(".llsd", ""); +        NamedTempFile file("llsd", "");          python("write Python notation",                 lambda::_1 << diff --git a/indra/llcommon/tests/llstreamqueue_test.cpp b/indra/llcommon/tests/llstreamqueue_test.cpp new file mode 100644 index 0000000000..050ad5c5bf --- /dev/null +++ b/indra/llcommon/tests/llstreamqueue_test.cpp @@ -0,0 +1,197 @@ +/** + * @file   llstreamqueue_test.cpp + * @author Nat Goodspeed + * @date   2012-01-05 + * @brief  Test for llstreamqueue. + *  + * $LicenseInfo:firstyear=2012&license=viewerlgpl$ + * Copyright (c) 2012, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "llstreamqueue.h" +// STL headers +#include <vector> +// std headers +// external library headers +#include <boost/foreach.hpp> +// other Linden headers +#include "../test/lltut.h" +#include "stringize.h" + +/***************************************************************************** +*   TUT +*****************************************************************************/ +namespace tut +{ +    struct llstreamqueue_data +    { +        llstreamqueue_data(): +            // we want a buffer with actual bytes in it, not an empty vector +            buffer(10) +        {} +        // As LLStreamQueue is merely a typedef for +        // LLGenericStreamQueue<char>, and no logic in LLGenericStreamQueue is +        // specific to the <char> instantiation, we're comfortable for now +        // testing only the narrow-char version. +        LLStreamQueue strq; +        // buffer for use in multiple tests +        std::vector<char> buffer; +    }; +    typedef test_group<llstreamqueue_data> llstreamqueue_group; +    typedef llstreamqueue_group::object object; +    llstreamqueue_group llstreamqueuegrp("llstreamqueue"); + +    template<> template<> +    void object::test<1>() +    { +        set_test_name("empty LLStreamQueue"); +        ensure_equals("brand-new LLStreamQueue isn't empty", +                      strq.size(), 0); +        ensure_equals("brand-new LLStreamQueue returns data", +                      strq.asSource().read(&buffer[0], buffer.size()), 0); +        strq.asSink().close(); +        ensure_equals("closed empty LLStreamQueue not at EOF", +                      strq.asSource().read(&buffer[0], buffer.size()), -1); +    } + +    template<> template<> +    void object::test<2>() +    { +        set_test_name("one internal block, one buffer"); +        LLStreamQueue::Sink sink(strq.asSink()); +        ensure_equals("write(\"\")", sink.write("", 0), 0); +        ensure_equals("0 write should leave LLStreamQueue empty (size())", +                      strq.size(), 0); +        ensure_equals("0 write should leave LLStreamQueue empty (peek())", +                      strq.peek(&buffer[0], buffer.size()), 0); +        // The meaning of "atomic" is that it must be smaller than our buffer. +        std::string atomic("atomic"); +        ensure("test data exceeds buffer", atomic.length() < buffer.size()); +        ensure_equals(STRINGIZE("write(\"" << atomic << "\")"), +                      sink.write(&atomic[0], atomic.length()), atomic.length()); +        ensure_equals("size() after write()", strq.size(), atomic.length()); +        size_t peeklen(strq.peek(&buffer[0], buffer.size())); +        ensure_equals(STRINGIZE("peek(\"" << atomic << "\")"), +                      peeklen, atomic.length()); +        ensure_equals(STRINGIZE("peek(\"" << atomic << "\") result"), +                      std::string(buffer.begin(), buffer.begin() + peeklen), atomic); +        ensure_equals("size() after peek()", strq.size(), atomic.length()); +        // peek() should not consume. Use a different buffer to prove it isn't +        // just leftover data from the first peek(). +        std::vector<char> again(buffer.size()); +        peeklen = size_t(strq.peek(&again[0], again.size())); +        ensure_equals(STRINGIZE("peek(\"" << atomic << "\") again"), +                      peeklen, atomic.length()); +        ensure_equals(STRINGIZE("peek(\"" << atomic << "\") again result"), +                      std::string(again.begin(), again.begin() + peeklen), atomic); +        // now consume. +        std::vector<char> third(buffer.size()); +        size_t readlen(strq.read(&third[0], third.size())); +        ensure_equals(STRINGIZE("read(\"" << atomic << "\")"), +                      readlen, atomic.length()); +        ensure_equals(STRINGIZE("read(\"" << atomic << "\") result"), +                      std::string(third.begin(), third.begin() + readlen), atomic); +        ensure_equals("peek() after read()", strq.peek(&buffer[0], buffer.size()), 0); +        ensure_equals("size() after read()", strq.size(), 0); +    } + +    template<> template<> +    void object::test<3>() +    { +        set_test_name("basic skip()"); +        std::string lovecraft("lovecraft"); +        ensure("test data exceeds buffer", lovecraft.length() < buffer.size()); +        ensure_equals(STRINGIZE("write(\"" << lovecraft << "\")"), +                      strq.write(&lovecraft[0], lovecraft.length()), lovecraft.length()); +        size_t peeklen(strq.peek(&buffer[0], buffer.size())); +        ensure_equals(STRINGIZE("peek(\"" << lovecraft << "\")"), +                      peeklen, lovecraft.length()); +        ensure_equals(STRINGIZE("peek(\"" << lovecraft << "\") result"), +                      std::string(buffer.begin(), buffer.begin() + peeklen), lovecraft); +        std::streamsize skip1(4); +        ensure_equals(STRINGIZE("skip(" << skip1 << ")"), strq.skip(skip1), skip1); +        ensure_equals("size() after skip()", strq.size(), lovecraft.length() - skip1); +        size_t readlen(strq.read(&buffer[0], buffer.size())); +        ensure_equals(STRINGIZE("read(\"" << lovecraft.substr(skip1) << "\")"), +                      readlen, lovecraft.length() - skip1); +        ensure_equals(STRINGIZE("read(\"" << lovecraft.substr(skip1) << "\") result"), +                      std::string(buffer.begin(), buffer.begin() + readlen), +                      lovecraft.substr(skip1)); +        ensure_equals("unconsumed", strq.read(&buffer[0], buffer.size()), 0); +    } + +    template<> template<> +    void object::test<4>() +    { +        set_test_name("skip() multiple blocks"); +        std::string blocks[] = { "books of ", "H.P. ", "Lovecraft" }; +        std::streamsize total(blocks[0].length() + blocks[1].length() + blocks[2].length()); +        std::streamsize leave(5);   // len("craft") above +        std::streamsize skip(total - leave); +        std::streamsize written(0); +        BOOST_FOREACH(const std::string& block, blocks) +        { +            written += strq.write(&block[0], block.length()); +            ensure_equals("size() after write()", strq.size(), written); +        } +        std::streamsize skiplen(strq.skip(skip)); +        ensure_equals(STRINGIZE("skip(" << skip << ")"), skiplen, skip); +        ensure_equals("size() after skip()", strq.size(), leave); +        size_t readlen(strq.read(&buffer[0], buffer.size())); +        ensure_equals("read(\"craft\")", readlen, leave); +        ensure_equals("read(\"craft\") result", +                      std::string(buffer.begin(), buffer.begin() + readlen), "craft"); +    } + +    template<> template<> +    void object::test<5>() +    { +        set_test_name("concatenate blocks"); +        std::string blocks[] = { "abcd", "efghij", "klmnopqrs" }; +        BOOST_FOREACH(const std::string& block, blocks) +        { +            strq.write(&block[0], block.length()); +        } +        std::vector<char> longbuffer(30); +        std::streamsize readlen(strq.read(&longbuffer[0], longbuffer.size())); +        ensure_equals("read() multiple blocks", +                      readlen, blocks[0].length() + blocks[1].length() + blocks[2].length()); +        ensure_equals("read() multiple blocks result", +                      std::string(longbuffer.begin(), longbuffer.begin() + readlen), +                      blocks[0] + blocks[1] + blocks[2]); +    } + +    template<> template<> +    void object::test<6>() +    { +        set_test_name("split blocks"); +        std::string blocks[] = { "abcdefghijklm", "nopqrstuvwxyz" }; +        BOOST_FOREACH(const std::string& block, blocks) +        { +            strq.write(&block[0], block.length()); +        } +        strq.close(); +        // We've already verified what strq.size() should be at this point; +        // see above test named "skip() multiple blocks" +        std::streamsize chksize(strq.size()); +        std::streamsize readlen(strq.read(&buffer[0], buffer.size())); +        ensure_equals("read() 0", readlen, buffer.size()); +        ensure_equals("read() 0 result", std::string(buffer.begin(), buffer.end()), "abcdefghij"); +        chksize -= readlen; +        ensure_equals("size() after read() 0", strq.size(), chksize); +        readlen = strq.read(&buffer[0], buffer.size()); +        ensure_equals("read() 1", readlen, buffer.size()); +        ensure_equals("read() 1 result", std::string(buffer.begin(), buffer.end()), "klmnopqrst"); +        chksize -= readlen; +        ensure_equals("size() after read() 1", strq.size(), chksize); +        readlen = strq.read(&buffer[0], buffer.size()); +        ensure_equals("read() 2", readlen, chksize); +        ensure_equals("read() 2 result", +                      std::string(buffer.begin(), buffer.begin() + readlen), "uvwxyz"); +        ensure_equals("read() 3", strq.read(&buffer[0], buffer.size()), -1); +    } +} // namespace tut diff --git a/indra/llcommon/tests/llstring_test.cpp b/indra/llcommon/tests/llstring_test.cpp index 6a1cbf652a..93d3968dbf 100644 --- a/indra/llcommon/tests/llstring_test.cpp +++ b/indra/llcommon/tests/llstring_test.cpp @@ -29,7 +29,11 @@  #include "linden_common.h"  #include "../test/lltut.h" +#include <boost/assign/list_of.hpp>  #include "../llstring.h" +#include "StringVec.h" + +using boost::assign::list_of;  namespace tut  { @@ -750,4 +754,118 @@ namespace tut  		ensure("empty substr.", !LLStringUtil::endsWith(empty, value));  		ensure("empty everything.", !LLStringUtil::endsWith(empty, empty));  	} + +	template<> template<> +	void string_index_object_t::test<41>() +	{ +		set_test_name("getTokens(\"delims\")"); +		ensure_equals("empty string", LLStringUtil::getTokens("", " "), StringVec()); +		ensure_equals("only delims", +					  LLStringUtil::getTokens("   \r\n   ", " \r\n"), StringVec()); +		ensure_equals("sequence of delims", +					  LLStringUtil::getTokens(",,, one ,,,", ","), list_of("one")); +		// nat considers this a dubious implementation side effect, but I'd +		// hate to change it now... +		ensure_equals("noncontiguous tokens", +					  LLStringUtil::getTokens(", ,, , one ,,,", ","), list_of("")("")("one")); +		ensure_equals("space-padded tokens", +					  LLStringUtil::getTokens(",    one  ,  two  ,", ","), list_of("one")("two")); +		ensure_equals("no delims", LLStringUtil::getTokens("one", ","), list_of("one")); +	} + +	// Shorthand for verifying that getTokens() behaves the same when you +	// don't pass a string of escape characters, when you pass an empty string +	// (different overloads), and when you pass a string of characters that +	// aren't actually present. +	void ensure_getTokens(const std::string& desc, +						  const std::string& string, +						  const std::string& drop_delims, +						  const std::string& keep_delims, +						  const std::string& quotes, +						  const std::vector<std::string>& expect) +	{ +		ensure_equals(desc + " - no esc", +					  LLStringUtil::getTokens(string, drop_delims, keep_delims, quotes), +					  expect); +		ensure_equals(desc + " - empty esc", +					  LLStringUtil::getTokens(string, drop_delims, keep_delims, quotes, ""), +					  expect); +		ensure_equals(desc + " - unused esc", +					  LLStringUtil::getTokens(string, drop_delims, keep_delims, quotes, "!"), +					  expect); +	} + +	void ensure_getTokens(const std::string& desc, +						  const std::string& string, +						  const std::string& drop_delims, +						  const std::string& keep_delims, +						  const std::vector<std::string>& expect) +	{ +		ensure_getTokens(desc, string, drop_delims, keep_delims, "", expect); +	} + +	template<> template<> +	void string_index_object_t::test<42>() +	{ +		set_test_name("getTokens(\"delims\", etc.)"); +		// Signatures to test in this method: +		// getTokens(string, drop_delims, keep_delims [, quotes [, escapes]]) +		// If you omit keep_delims, you get the older function (test above). + +		// cases like the getTokens(string, delims) tests above +		ensure_getTokens("empty string", "", " ", "", StringVec()); +		ensure_getTokens("only delims", +						 "   \r\n   ", " \r\n", "", StringVec()); +		ensure_getTokens("sequence of delims", +						 ",,, one ,,,", ", ", "", list_of("one")); +		// Note contrast with the case in the previous method +		ensure_getTokens("noncontiguous tokens", +						 ", ,, , one ,,,", ", ", "", list_of("one")); +		ensure_getTokens("space-padded tokens", +						 ",    one  ,  two  ,", ", ", "", +                         list_of("one")("two")); +		ensure_getTokens("no delims", "one", ",", "", list_of("one")); + +		// drop_delims vs. keep_delims +		ensure_getTokens("arithmetic", +						 " ab+def  / xx*  yy ", " ", "+-*/", +						 list_of("ab")("+")("def")("/")("xx")("*")("yy")); + +		// quotes +		ensure_getTokens("no quotes", +						 "She said, \"Don't go.\"", " ", ",", "", +						 list_of("She")("said")(",")("\"Don't")("go.\"")); +		ensure_getTokens("quotes", +						 "She said, \"Don't go.\"", " ", ",", "\"", +						 list_of("She")("said")(",")("Don't go.")); +		ensure_getTokens("quotes and delims", +						 "run c:/'Documents and Settings'/someone", " ", "", "'", +						 list_of("run")("c:/Documents and Settings/someone")); +		ensure_getTokens("unmatched quote", +						 "baby don't leave", " ", "", "'", +						 list_of("baby")("don't")("leave")); +		ensure_getTokens("adjacent quoted", +						 "abc'def \"ghi'\"jkl' mno\"pqr", " ", "", "\"'", +						 list_of("abcdef \"ghijkl' mnopqr")); +		ensure_getTokens("quoted empty string", +						 "--set SomeVar ''", " ", "", "'", +						 list_of("--set")("SomeVar")("")); + +		// escapes +		// Don't use backslash as an escape for these tests -- you'll go nuts +		// between the C++ string scanner and getTokens() escapes. Test with +		// something else! +		ensure_equals("escaped delims", +					  LLStringUtil::getTokens("^ a - dog^-gone^ phrase", " ", "-", "", "^"), +					  list_of(" a")("-")("dog-gone phrase")); +		ensure_equals("escaped quotes", +					  LLStringUtil::getTokens("say: 'this isn^'t w^orking'.", " ", "", "'", "^"), +					  list_of("say:")("this isn't working.")); +		ensure_equals("escaped escape", +					  LLStringUtil::getTokens("want x^^2", " ", "", "", "^"), +					  list_of("want")("x^2")); +		ensure_equals("escape at end", +					  LLStringUtil::getTokens("it's^ up there^", " ", "", "'", "^"), +					  list_of("it's up")("there^")); +    }  } diff --git a/indra/llcommon/tests/setpython.py b/indra/llcommon/tests/setpython.py deleted file mode 100644 index df7b90428e..0000000000 --- a/indra/llcommon/tests/setpython.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/python -"""\ -@file   setpython.py -@author Nat Goodspeed -@date   2011-07-13 -@brief  Set PYTHON environment variable for tests that care. - -$LicenseInfo:firstyear=2011&license=viewerlgpl$ -Copyright (c) 2011, Linden Research, Inc. -$/LicenseInfo$ -""" - -import os -import sys -import subprocess - -if __name__ == "__main__": -    os.environ["PYTHON"] = sys.executable -    sys.exit(subprocess.call(sys.argv[1:])) diff --git a/indra/llcommon/tests/wrapllerrs.h b/indra/llcommon/tests/wrapllerrs.h index ffda84729b..f79acacb22 100644 --- a/indra/llcommon/tests/wrapllerrs.h +++ b/indra/llcommon/tests/wrapllerrs.h @@ -29,7 +29,17 @@  #if ! defined(LL_WRAPLLERRS_H)  #define LL_WRAPLLERRS_H +#include <tut/tut.hpp>  #include "llerrorcontrol.h" +#include "stringize.h" +#include <boost/bind.hpp> +#include <list> +#include <string> +#include <stdexcept> + +// statically reference the function in test.cpp... it's short, we could +// replicate, but better to reuse +extern void wouldHaveCrashed(const std::string& message);  struct WrapLL_ERRS  { @@ -70,4 +80,87 @@ struct WrapLL_ERRS      LLError::FatalFunction mPriorFatal;  }; +/** + * Capture log messages. This is adapted (simplified) from the one in + * llerror_test.cpp. + */ +class CaptureLog : public LLError::Recorder +{ +public: +    CaptureLog(LLError::ELevel level=LLError::LEVEL_DEBUG): +        // Mostly what we're trying to accomplish by saving and resetting +        // LLError::Settings is to bypass the default RecordToStderr and +        // RecordToWinDebug Recorders. As these are visible only inside +        // llerror.cpp, we can't just call LLError::removeRecorder() with +        // each. For certain tests we need to produce, capture and examine +        // DEBUG log messages -- but we don't want to spam the user's console +        // with that output. If it turns out that saveAndResetSettings() has +        // some bad effect, give up and just let the DEBUG level log messages +        // display. +        mOldSettings(LLError::saveAndResetSettings()) +    { +        LLError::setFatalFunction(wouldHaveCrashed); +        LLError::setDefaultLevel(level); +        LLError::addRecorder(this); +    } + +    ~CaptureLog() +    { +        LLError::removeRecorder(this); +        LLError::restoreSettings(mOldSettings); +    } + +    void recordMessage(LLError::ELevel level, +                       const std::string& message) +    { +        mMessages.push_back(message); +    } + +    /// Don't assume the message we want is necessarily the LAST log message +    /// emitted by the underlying code; search backwards through all messages +    /// for the sought string. +    std::string messageWith(const std::string& search, bool required=true) +    { +        for (MessageList::const_reverse_iterator rmi(mMessages.rbegin()), rmend(mMessages.rend()); +             rmi != rmend; ++rmi) +        { +            if (rmi->find(search) != std::string::npos) +                return *rmi; +        } +        // failed to find any such message +        if (! required) +            return std::string(); + +        throw tut::failure(STRINGIZE("failed to find '" << search +                                     << "' in captured log messages:\n" +                                     << *this)); +    } + +    std::ostream& streamto(std::ostream& out) const +    { +        MessageList::const_iterator mi(mMessages.begin()), mend(mMessages.end()); +        if (mi != mend) +        { +            // handle first message separately: it doesn't get a newline +            out << *mi++; +            for ( ; mi != mend; ++mi) +            { +                // every subsequent message gets a newline +                out << '\n' << *mi; +            } +        } +        return out; +    } + +    typedef std::list<std::string> MessageList; +    MessageList mMessages; +    LLError::Settings* mOldSettings; +}; + +inline +std::ostream& operator<<(std::ostream& out, const CaptureLog& log) +{ +    return log.streamto(out); +} +  #endif /* ! defined(LL_WRAPLLERRS_H) */ | 
