diff options
Diffstat (limited to 'indra/llcommon')
37 files changed, 10329 insertions, 1035 deletions
diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index 0a3eaec5c5..dd7b8c6eb8 100644 --- a/indra/llcommon/CMakeLists.txt +++ b/indra/llcommon/CMakeLists.txt @@ -61,7 +61,10 @@ set(llcommon_SOURCE_FILES llformat.cpp llframetimer.cpp llheartbeat.cpp + llinitparam.cpp llinstancetracker.cpp + llleap.cpp + llleaplistener.cpp llliveappconfig.cpp lllivefile.cpp lllog.cpp @@ -74,13 +77,14 @@ set(llcommon_SOURCE_FILES llmortician.cpp lloptioninterface.cpp llptrto.cpp - llprocesslauncher.cpp + llprocess.cpp llprocessor.cpp llqueuedthread.cpp llrand.cpp llrefcount.cpp llrun.cpp llsd.cpp + llsdparam.cpp llsdserialize.cpp llsdserialize_xml.cpp llsdutil.cpp @@ -88,6 +92,7 @@ set(llcommon_SOURCE_FILES llsingleton.cpp llstat.cpp llstacktrace.cpp + llstreamqueue.cpp llstreamtools.cpp llstring.cpp llstringtable.cpp @@ -173,9 +178,12 @@ set(llcommon_HEADER_FILES llheartbeat.h llhttpstatuscodes.h llindexedqueue.h + llinitparam.h llinstancetracker.h llkeythrottle.h lllazy.h + llleap.h + llleaplistener.h lllistenerwrapper.h lllinkedqueue.h llliveappconfig.h @@ -196,7 +204,7 @@ set(llcommon_HEADER_FILES llpointer.h llpreprocessor.h llpriqueuemap.h - llprocesslauncher.h + llprocess.h llprocessor.h llptrskiplist.h llptrskipmap.h @@ -204,10 +212,12 @@ set(llcommon_HEADER_FILES llqueuedthread.h llrand.h llrefcount.h + llregistry.h llrun.h llrefcount.h llsafehandle.h llsd.h + llsdparam.h llsdserialize.h llsdserialize_xml.h llsdutil.h @@ -216,11 +226,13 @@ set(llcommon_HEADER_FILES llsingleton.h llskiplist.h llskipmap.h + llsortedvector.h llstack.h llstacktrace.h llstat.h llstatenums.h llstl.h + llstreamqueue.h llstreamtools.h llstrider.h llstring.h @@ -230,6 +242,7 @@ set(llcommon_HEADER_FILES llthreadsafequeue.h lltimer.h lltreeiterators.h + lltypeinfolookup.h lluri.h lluuid.h lluuidhashmap.h @@ -317,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}") @@ -326,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 e4381dbbd6..7e6eee0f3c 100644 --- a/indra/llcommon/llerror.cpp +++ b/indra/llcommon/llerror.cpp @@ -654,9 +654,7 @@ namespace LLError g.invalidateCallSites(); s.tagLevelMap[tag_name] = level; } -} -namespace { LLError::ELevel decodeLevel(std::string name) { static LevelMap level_names; @@ -681,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 ed9de002f5..d53a819d88 100644 --- a/indra/llcommon/llerrorcontrol.h +++ b/indra/llcommon/llerrorcontrol.h @@ -1,4 +1,4 @@ -/** +/** * @file llerrorcontrol.h * @date December 2006 * @brief error message system control @@ -6,21 +6,21 @@ * $LicenseInfo:firstyear=2007&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$ */ @@ -38,7 +38,7 @@ class LLSD; This is the part of the LLError namespace that manages the messages produced by the logging. The logging support is defined in llerror.h. Most files do not need to include this. - + These implementations are in llerror.cpp. */ @@ -72,7 +72,7 @@ namespace LLError Settings that control what is logged. Setting a level means log messages at that level or above. */ - + LL_COMMON_API void setPrintLocation(bool); LL_COMMON_API void setDefaultLevel(LLError::ELevel); LL_COMMON_API ELevel getDefaultLevel(); @@ -80,7 +80,8 @@ namespace LLError 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 @@ -100,31 +101,31 @@ namespace LLError // (by, for example, setting a class level to LEVEL_NONE), will keep // the that message from causing the fatal funciton to be invoked. - LL_COMMON_API FatalFunction getFatalFunction(); - // Retrieve the previously-set FatalFunction - - /// temporarily override the FatalFunction for the duration of a - /// particular scope, e.g. for unit tests - class LL_COMMON_API OverrideFatalFunction - { - public: - OverrideFatalFunction(const FatalFunction& func): - mPrev(getFatalFunction()) - { - setFatalFunction(func); - } - ~OverrideFatalFunction() - { - setFatalFunction(mPrev); - } - - private: - FatalFunction mPrev; - }; + LL_COMMON_API FatalFunction getFatalFunction(); + // Retrieve the previously-set FatalFunction + + /// temporarily override the FatalFunction for the duration of a + /// particular scope, e.g. for unit tests + class LL_COMMON_API OverrideFatalFunction + { + public: + OverrideFatalFunction(const FatalFunction& func): + mPrev(getFatalFunction()) + { + setFatalFunction(func); + } + ~OverrideFatalFunction() + { + setFatalFunction(mPrev); + } + + private: + FatalFunction mPrev; + }; typedef std::string (*TimeFunction)(); LL_COMMON_API std::string utcTime(); - + LL_COMMON_API void setTimeFunction(TimeFunction); // The function is use to return the current time, formatted for // display by those error recorders that want the time included. @@ -136,19 +137,27 @@ namespace LLError // An object that handles the actual output or error messages. public: virtual ~Recorder(); - + virtual void recordMessage(LLError::ELevel, const std::string& message) = 0; // use the level for better display, not for filtering - + virtual bool wantsTime(); // default returns false // override and return true if the recorder wants the time string // included in the text of the message }; - + + /** + * @NOTE: addRecorder() conveys ownership to the underlying Settings + * object -- when destroyed, it will @em delete the passed Recorder*! + */ LL_COMMON_API void addRecorder(Recorder*); + /** + * @NOTE: removeRecorder() reclaims ownership of the Recorder*: its + * lifespan becomes the caller's problem. + */ LL_COMMON_API void removeRecorder(Recorder*); // each error message is passed to each recorder via recordMessage() - + LL_COMMON_API void logToFile(const std::string& filename); LL_COMMON_API void logToFixedBuffer(LLLineBuffer*); // Utilities to add recorders for logging to a file or a fixed buffer @@ -166,10 +175,9 @@ namespace LLError class Settings; LL_COMMON_API Settings* saveAndResetSettings(); LL_COMMON_API void restoreSettings(Settings *); - + LL_COMMON_API std::string abbreviateFile(const std::string& filePath); LL_COMMON_API int shouldLogCallCount(); - }; #endif // LL_LLERRORCONTROL_H 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/llfile.cpp b/indra/llcommon/llfile.cpp index c32a776c3f..c51d042a3d 100644 --- a/indra/llcommon/llfile.cpp +++ b/indra/llcommon/llfile.cpp @@ -1,4 +1,4 @@ -/** +/** * @file llfile.cpp * @author Michael Schlachter * @date 2006-03-23 @@ -8,60 +8,194 @@ * $LicenseInfo:firstyear=2006&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$ */ #if LL_WINDOWS #include <windows.h> +#include <stdlib.h> // Windows errno +#else +#include <errno.h> #endif #include "linden_common.h" #include "llfile.h" #include "llstring.h" #include "llerror.h" +#include "stringize.h" using namespace std; +static std::string empty; + +// Many of the methods below use OS-level functions that mess with errno. Wrap +// variants of strerror() to report errors. + +#if LL_WINDOWS +// On Windows, use strerror_s(). +std::string strerr(int errn) +{ + char buffer[256]; + strerror_s(buffer, errn); // infers sizeof(buffer) -- love it! + return buffer; +} + +#else +// On Posix 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*/, size_t /*bufflen*/, + const char* strerror_ret) +{ + return strerror_ret; +} + +// strerror_r() returns int +std::string message_from(int orig_errno, const char* buffer, size_t bufflen, + 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 + << " (" << bufflen << "-byte 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 << ')'); +} + +std::string strerr(int errn) +{ + char buffer[256]; + // Select message_from() function matching the strerror_r() we have on hand. + return message_from(errn, buffer, sizeof(buffer), + strerror_r(errn, buffer, sizeof(buffer))); +} +#endif // ! LL_WINDOWS + +// On either system, shorthand call just infers global 'errno'. +std::string strerr() +{ + return strerr(errno); +} + +int warnif(const std::string& desc, const std::string& filename, int rc, int accept=0) +{ + if (rc < 0) + { + // Capture errno before we start emitting output + int errn = errno; + // For certain operations, a particular errno value might be + // acceptable -- e.g. stat() could permit ENOENT, mkdir() could permit + // EEXIST. Don't warn if caller explicitly says this errno is okay. + if (errn != accept) + { + LL_WARNS("LLFile") << "Couldn't " << desc << " '" << filename + << "' (errno " << errn << "): " << strerr(errn) << LL_ENDL; + } +#if 0 && LL_WINDOWS // turn on to debug file-locking problems + // If the problem is "Permission denied," maybe it's because another + // process has the file open. Try to find out. + if (errn == EACCES) // *not* EPERM + { + // Only do any of this stuff (before LL_ENDL) if it will be logged. + LL_DEBUGS("LLFile") << empty; + const char* TEMP = getenv("TEMP"); + if (! TEMP) + { + LL_CONT << "No $TEMP, not running 'handle'"; + } + else + { + std::string tf(TEMP); + tf += "\\handle.tmp"; + // http://technet.microsoft.com/en-us/sysinternals/bb896655 + std::string cmd(STRINGIZE("handle \"" << filename + // "openfiles /query /v | fgrep -i \"" << filename + << "\" > \"" << tf << '"')); + LL_CONT << cmd; + if (system(cmd.c_str()) != 0) + { + LL_CONT << "\nDownload 'handle.exe' from http://technet.microsoft.com/en-us/sysinternals/bb896655"; + } + else + { + std::ifstream inf(tf); + std::string line; + while (std::getline(inf, line)) + { + LL_CONT << '\n' << line; + } + } + LLFile::remove(tf); + } + LL_CONT << LL_ENDL; + } +#endif // LL_WINDOWS hack to identify processes holding file open + } + return rc; +} + // static int LLFile::mkdir(const std::string& dirname, int perms) { -#if LL_WINDOWS +#if LL_WINDOWS // permissions are ignored on Windows std::string utf8dirname = dirname; llutf16string utf16dirname = utf8str_to_utf16str(utf8dirname); - return _wmkdir(utf16dirname.c_str()); + int rc = _wmkdir(utf16dirname.c_str()); #else - return ::mkdir(dirname.c_str(), (mode_t)perms); + int rc = ::mkdir(dirname.c_str(), (mode_t)perms); #endif + // We often use mkdir() to ensure the existence of a directory that might + // already exist. Don't spam the log if it does. + return warnif("mkdir", dirname, rc, EEXIST); } // static int LLFile::rmdir(const std::string& dirname) { -#if LL_WINDOWS +#if LL_WINDOWS // permissions are ignored on Windows std::string utf8dirname = dirname; llutf16string utf16dirname = utf8str_to_utf16str(utf8dirname); - return _wrmdir(utf16dirname.c_str()); + int rc = _wrmdir(utf16dirname.c_str()); #else - return ::rmdir(dirname.c_str()); + int rc = ::rmdir(dirname.c_str()); #endif + return warnif("rmdir", dirname, rc); } // static @@ -108,10 +242,11 @@ int LLFile::remove(const std::string& filename) #if LL_WINDOWS std::string utf8filename = filename; llutf16string utf16filename = utf8str_to_utf16str(utf8filename); - return _wremove(utf16filename.c_str()); + int rc = _wremove(utf16filename.c_str()); #else - return ::remove(filename.c_str()); + int rc = ::remove(filename.c_str()); #endif + return warnif("remove", filename, rc); } int LLFile::rename(const std::string& filename, const std::string& newname) @@ -121,10 +256,11 @@ int LLFile::rename(const std::string& filename, const std::string& newname) std::string utf8newname = newname; llutf16string utf16filename = utf8str_to_utf16str(utf8filename); llutf16string utf16newname = utf8str_to_utf16str(utf8newname); - return _wrename(utf16filename.c_str(),utf16newname.c_str()); + int rc = _wrename(utf16filename.c_str(),utf16newname.c_str()); #else - return ::rename(filename.c_str(),newname.c_str()); + int rc = ::rename(filename.c_str(),newname.c_str()); #endif + return warnif(STRINGIZE("rename to '" << newname << "' from"), filename, rc); } int LLFile::stat(const std::string& filename, llstat* filestatus) @@ -132,23 +268,26 @@ int LLFile::stat(const std::string& filename, llstat* filestatus) #if LL_WINDOWS std::string utf8filename = filename; llutf16string utf16filename = utf8str_to_utf16str(utf8filename); - return _wstat(utf16filename.c_str(),filestatus); + int rc = _wstat(utf16filename.c_str(),filestatus); #else - return ::stat(filename.c_str(),filestatus); + int rc = ::stat(filename.c_str(),filestatus); #endif + // We use stat() to determine existence (see isfile(), isdir()). + // Don't spam the log if the subject pathname doesn't exist. + return warnif("stat", filename, rc, ENOENT); } bool LLFile::isdir(const std::string& filename) { llstat st; - + return stat(filename, &st) == 0 && S_ISDIR(st.st_mode); } bool LLFile::isfile(const std::string& filename) { llstat st; - + return stat(filename, &st) == 0 && S_ISREG(st.st_mode); } @@ -260,7 +399,7 @@ void llifstream::open(const std::string& _Filename, /* Flawfinder: ignore */ ios_base::openmode _Mode, int _Prot) { // open a C stream with specified mode - + LLFILE* filep = LLFile::_Fiopen(_Filename,_Mode | ios_base::in, _Prot); if(filep == NULL) { @@ -280,7 +419,7 @@ bool llifstream::is_open() const return false; } llifstream::~llifstream() -{ +{ if (_ShouldClose) { close(); @@ -309,7 +448,7 @@ bool llofstream::is_open() const void llofstream::open(const std::string& _Filename, /* Flawfinder: ignore */ ios_base::openmode _Mode, - int _Prot) + int _Prot) { // open a C stream with specified mode LLFILE* filep = LLFile::_Fiopen(_Filename,_Mode | ios_base::out, _Prot); @@ -340,14 +479,14 @@ void llofstream::close() llofstream::llofstream(const std::string& _Filename, std::ios_base::openmode _Mode, - int _Prot) + int _Prot) : std::basic_ostream<char,std::char_traits < char > >(NULL,true),_Filebuffer(NULL),_ShouldClose(false) { // construct with named file and specified mode open(_Filename, _Mode , _Prot); /* Flawfinder: ignore */ } llofstream::~llofstream() -{ +{ // destroy the object if (_ShouldClose) { diff --git a/indra/llcommon/llinitparam.cpp b/indra/llcommon/llinitparam.cpp new file mode 100644 index 0000000000..db72aa19b9 --- /dev/null +++ b/indra/llcommon/llinitparam.cpp @@ -0,0 +1,469 @@ +/** + * @file llinitparam.cpp + * @brief parameter block abstraction for creating complex objects and + * parsing construction parameters from xml and LLSD + * + * $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 "llinitparam.h" + + +namespace LLInitParam +{ + // + // Param + // + Param::Param(BaseBlock* enclosing_block) + : mIsProvided(false) + { + const U8* my_addr = reinterpret_cast<const U8*>(this); + const U8* block_addr = reinterpret_cast<const U8*>(enclosing_block); + mEnclosingBlockOffset = 0x7FFFffff & (U32)(my_addr - block_addr); + } + + // + // ParamDescriptor + // + ParamDescriptor::ParamDescriptor(param_handle_t p, + merge_func_t merge_func, + deserialize_func_t deserialize_func, + serialize_func_t serialize_func, + validation_func_t validation_func, + inspect_func_t inspect_func, + S32 min_count, + S32 max_count) + : mParamHandle(p), + mMergeFunc(merge_func), + mDeserializeFunc(deserialize_func), + mSerializeFunc(serialize_func), + mValidationFunc(validation_func), + mInspectFunc(inspect_func), + mMinCount(min_count), + mMaxCount(max_count), + mUserData(NULL) + {} + + ParamDescriptor::ParamDescriptor() + : mParamHandle(0), + mMergeFunc(NULL), + mDeserializeFunc(NULL), + mSerializeFunc(NULL), + mValidationFunc(NULL), + mInspectFunc(NULL), + mMinCount(0), + mMaxCount(0), + mUserData(NULL) + {} + + ParamDescriptor::~ParamDescriptor() + { + delete mUserData; + } + + // + // Parser + // + Parser::~Parser() + {} + + void Parser::parserWarning(const std::string& message) + { + if (mParseSilently) return; + llwarns << message << llendl; + } + + void Parser::parserError(const std::string& message) + { + if (mParseSilently) return; + llerrs << message << llendl; + } + + + // + // BlockDescriptor + // + void BlockDescriptor::aggregateBlockData(BlockDescriptor& src_block_data) + { + mNamedParams.insert(src_block_data.mNamedParams.begin(), src_block_data.mNamedParams.end()); + std::copy(src_block_data.mUnnamedParams.begin(), src_block_data.mUnnamedParams.end(), std::back_inserter(mUnnamedParams)); + std::copy(src_block_data.mValidationList.begin(), src_block_data.mValidationList.end(), std::back_inserter(mValidationList)); + std::copy(src_block_data.mAllParams.begin(), src_block_data.mAllParams.end(), std::back_inserter(mAllParams)); + } + + BlockDescriptor::BlockDescriptor() + : mMaxParamOffset(0), + mInitializationState(UNINITIALIZED), + mCurrentBlockPtr(NULL) + {} + + // called by each derived class in least to most derived order + void BaseBlock::init(BlockDescriptor& descriptor, BlockDescriptor& base_descriptor, size_t block_size) + { + descriptor.mCurrentBlockPtr = this; + descriptor.mMaxParamOffset = block_size; + + switch(descriptor.mInitializationState) + { + case BlockDescriptor::UNINITIALIZED: + // copy params from base class here + descriptor.aggregateBlockData(base_descriptor); + + descriptor.mInitializationState = BlockDescriptor::INITIALIZING; + break; + case BlockDescriptor::INITIALIZING: + descriptor.mInitializationState = BlockDescriptor::INITIALIZED; + break; + case BlockDescriptor::INITIALIZED: + // nothing to do + break; + } + } + + param_handle_t BaseBlock::getHandleFromParam(const Param* param) const + { + const U8* param_address = reinterpret_cast<const U8*>(param); + const U8* baseblock_address = reinterpret_cast<const U8*>(this); + return (param_address - baseblock_address); + } + + bool BaseBlock::submitValue(Parser::name_stack_t& name_stack, Parser& p, bool silent) + { + if (!deserializeBlock(p, std::make_pair(name_stack.begin(), name_stack.end()), true)) + { + if (!silent) + { + p.parserWarning(llformat("Failed to parse parameter \"%s\"", p.getCurrentElementName().c_str())); + } + return false; + } + return true; + } + + + bool BaseBlock::validateBlock(bool emit_errors) const + { + const BlockDescriptor& block_data = mostDerivedBlockDescriptor(); + for (BlockDescriptor::param_validation_list_t::const_iterator it = block_data.mValidationList.begin(); it != block_data.mValidationList.end(); ++it) + { + const Param* param = getParamFromHandle(it->first); + if (!it->second(param)) + { + if (emit_errors) + { + llwarns << "Invalid param \"" << getParamName(block_data, param) << "\"" << llendl; + } + return false; + } + } + return true; + } + + void BaseBlock::serializeBlock(Parser& parser, Parser::name_stack_t& name_stack, const LLInitParam::BaseBlock* diff_block) const + { + // named param is one like LLView::Params::follows + // unnamed param is like LLView::Params::rect - implicit + const BlockDescriptor& block_data = mostDerivedBlockDescriptor(); + + for (BlockDescriptor::param_list_t::const_iterator it = block_data.mUnnamedParams.begin(); + it != block_data.mUnnamedParams.end(); + ++it) + { + param_handle_t param_handle = (*it)->mParamHandle; + const Param* param = getParamFromHandle(param_handle); + ParamDescriptor::serialize_func_t serialize_func = (*it)->mSerializeFunc; + if (serialize_func) + { + const Param* diff_param = diff_block ? diff_block->getParamFromHandle(param_handle) : NULL; + // each param descriptor remembers its serial number + // so we can inspect the same param under different names + // and see that it has the same number + name_stack.push_back(std::make_pair("", true)); + serialize_func(*param, parser, name_stack, diff_param); + name_stack.pop_back(); + } + } + + for(BlockDescriptor::param_map_t::const_iterator it = block_data.mNamedParams.begin(); + it != block_data.mNamedParams.end(); + ++it) + { + param_handle_t param_handle = it->second->mParamHandle; + const Param* param = getParamFromHandle(param_handle); + ParamDescriptor::serialize_func_t serialize_func = it->second->mSerializeFunc; + if (serialize_func && param->anyProvided()) + { + // Ensure this param has not already been serialized + // Prevents <rect> from being serialized as its own tag. + bool duplicate = false; + for (BlockDescriptor::param_list_t::const_iterator it2 = block_data.mUnnamedParams.begin(); + it2 != block_data.mUnnamedParams.end(); + ++it2) + { + if (param_handle == (*it2)->mParamHandle) + { + duplicate = true; + break; + } + } + + //FIXME: for now, don't attempt to serialize values under synonyms, as current parsers + // don't know how to detect them + if (duplicate) + { + continue; + } + + name_stack.push_back(std::make_pair(it->first, !duplicate)); + const Param* diff_param = diff_block ? diff_block->getParamFromHandle(param_handle) : NULL; + serialize_func(*param, parser, name_stack, diff_param); + name_stack.pop_back(); + } + } + } + + bool BaseBlock::inspectBlock(Parser& parser, Parser::name_stack_t name_stack, S32 min_count, S32 max_count) const + { + // named param is one like LLView::Params::follows + // unnamed param is like LLView::Params::rect - implicit + const BlockDescriptor& block_data = mostDerivedBlockDescriptor(); + + for (BlockDescriptor::param_list_t::const_iterator it = block_data.mUnnamedParams.begin(); + it != block_data.mUnnamedParams.end(); + ++it) + { + param_handle_t param_handle = (*it)->mParamHandle; + const Param* param = getParamFromHandle(param_handle); + ParamDescriptor::inspect_func_t inspect_func = (*it)->mInspectFunc; + if (inspect_func) + { + name_stack.push_back(std::make_pair("", true)); + inspect_func(*param, parser, name_stack, (*it)->mMinCount, (*it)->mMaxCount); + name_stack.pop_back(); + } + } + + for(BlockDescriptor::param_map_t::const_iterator it = block_data.mNamedParams.begin(); + it != block_data.mNamedParams.end(); + ++it) + { + param_handle_t param_handle = it->second->mParamHandle; + const Param* param = getParamFromHandle(param_handle); + ParamDescriptor::inspect_func_t inspect_func = it->second->mInspectFunc; + if (inspect_func) + { + // Ensure this param has not already been inspected + bool duplicate = false; + for (BlockDescriptor::param_list_t::const_iterator it2 = block_data.mUnnamedParams.begin(); + it2 != block_data.mUnnamedParams.end(); + ++it2) + { + if (param_handle == (*it2)->mParamHandle) + { + duplicate = true; + break; + } + } + + name_stack.push_back(std::make_pair(it->first, !duplicate)); + inspect_func(*param, parser, name_stack, it->second->mMinCount, it->second->mMaxCount); + name_stack.pop_back(); + } + } + + return true; + } + + bool BaseBlock::deserializeBlock(Parser& p, Parser::name_stack_range_t name_stack_range, bool ignored) + { + BlockDescriptor& block_data = mostDerivedBlockDescriptor(); + bool names_left = name_stack_range.first != name_stack_range.second; + + bool new_name = names_left + ? name_stack_range.first->second + : true; + + if (names_left) + { + const std::string& top_name = name_stack_range.first->first; + + ParamDescriptor::deserialize_func_t deserialize_func = NULL; + Param* paramp = NULL; + + BlockDescriptor::param_map_t::iterator found_it = block_data.mNamedParams.find(top_name); + if (found_it != block_data.mNamedParams.end()) + { + // find pointer to member parameter from offset table + paramp = getParamFromHandle(found_it->second->mParamHandle); + deserialize_func = found_it->second->mDeserializeFunc; + + Parser::name_stack_range_t new_name_stack(name_stack_range.first, name_stack_range.second); + ++new_name_stack.first; + if (deserialize_func(*paramp, p, new_name_stack, new_name)) + { + // value is no longer new, we know about it now + name_stack_range.first->second = false; + return true; + } + else + { + return false; + } + } + } + + // try to parse unnamed parameters, in declaration order + for ( BlockDescriptor::param_list_t::iterator it = block_data.mUnnamedParams.begin(); + it != block_data.mUnnamedParams.end(); + ++it) + { + Param* paramp = getParamFromHandle((*it)->mParamHandle); + ParamDescriptor::deserialize_func_t deserialize_func = (*it)->mDeserializeFunc; + + if (deserialize_func && deserialize_func(*paramp, p, name_stack_range, new_name)) + { + return true; + } + } + + // if no match, and no names left on stack, this is just an existence assertion of this block + // verify by calling readValue with NoParamValue type, an inherently unparseable type + if (!names_left) + { + Flag no_value; + return p.readValue(no_value); + } + + return false; + } + + //static + void BaseBlock::addParam(BlockDescriptor& block_data, const ParamDescriptorPtr in_param, const char* char_name) + { + // create a copy of the param descriptor in mAllParams + // so other data structures can store a pointer to it + block_data.mAllParams.push_back(in_param); + ParamDescriptorPtr param(block_data.mAllParams.back()); + + std::string name(char_name); + if ((size_t)param->mParamHandle > block_data.mMaxParamOffset) + { + llerrs << "Attempted to register param with block defined for parent class, make sure to derive from LLInitParam::Block<YOUR_CLASS, PARAM_BLOCK_BASE_CLASS>" << llendl; + } + + if (name.empty()) + { + block_data.mUnnamedParams.push_back(param); + } + else + { + // don't use insert, since we want to overwrite existing entries + block_data.mNamedParams[name] = param; + } + + if (param->mValidationFunc) + { + block_data.mValidationList.push_back(std::make_pair(param->mParamHandle, param->mValidationFunc)); + } + } + + void BaseBlock::addSynonym(Param& param, const std::string& synonym) + { + BlockDescriptor& block_data = mostDerivedBlockDescriptor(); + if (block_data.mInitializationState == BlockDescriptor::INITIALIZING) + { + param_handle_t handle = getHandleFromParam(¶m); + + // check for invalid derivation from a paramblock (i.e. without using + // Block<T, Base_Class> + if ((size_t)handle > block_data.mMaxParamOffset) + { + llerrs << "Attempted to register param with block defined for parent class, make sure to derive from LLInitParam::Block<YOUR_CLASS, PARAM_BLOCK_BASE_CLASS>" << llendl; + } + + ParamDescriptorPtr param_descriptor = findParamDescriptor(param); + if (param_descriptor) + { + if (synonym.empty()) + { + block_data.mUnnamedParams.push_back(param_descriptor); + } + else + { + block_data.mNamedParams[synonym] = param_descriptor; + } + } + } + } + + const std::string& BaseBlock::getParamName(const BlockDescriptor& block_data, const Param* paramp) const + { + param_handle_t handle = getHandleFromParam(paramp); + for (BlockDescriptor::param_map_t::const_iterator it = block_data.mNamedParams.begin(); it != block_data.mNamedParams.end(); ++it) + { + if (it->second->mParamHandle == handle) + { + return it->first; + } + } + + return LLStringUtil::null; + } + + ParamDescriptorPtr BaseBlock::findParamDescriptor(const Param& param) + { + param_handle_t handle = getHandleFromParam(¶m); + BlockDescriptor& descriptor = mostDerivedBlockDescriptor(); + BlockDescriptor::all_params_list_t::iterator end_it = descriptor.mAllParams.end(); + for (BlockDescriptor::all_params_list_t::iterator it = descriptor.mAllParams.begin(); + it != end_it; + ++it) + { + if ((*it)->mParamHandle == handle) return *it; + } + return ParamDescriptorPtr(); + } + + // take all provided params from other and apply to self + // NOTE: this requires that "other" is of the same derived type as this + bool BaseBlock::mergeBlock(BlockDescriptor& block_data, const BaseBlock& other, bool overwrite) + { + bool some_param_changed = false; + BlockDescriptor::all_params_list_t::const_iterator end_it = block_data.mAllParams.end(); + for (BlockDescriptor::all_params_list_t::const_iterator it = block_data.mAllParams.begin(); + it != end_it; + ++it) + { + const Param* other_paramp = other.getParamFromHandle((*it)->mParamHandle); + ParamDescriptor::merge_func_t merge_func = (*it)->mMergeFunc; + if (merge_func) + { + Param* paramp = getParamFromHandle((*it)->mParamHandle); + llassert(paramp->mEnclosingBlockOffset == (*it)->mParamHandle); + some_param_changed |= merge_func(*paramp, *other_paramp, overwrite); + } + } + return some_param_changed; + } +} diff --git a/indra/llcommon/llinitparam.h b/indra/llcommon/llinitparam.h new file mode 100644 index 0000000000..99983a19cb --- /dev/null +++ b/indra/llcommon/llinitparam.h @@ -0,0 +1,2300 @@ +/** + * @file llinitparam.h + * @brief parameter block abstraction for creating complex objects and + * parsing construction parameters from xml and LLSD + * + * $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_LLPARAM_H +#define LL_LLPARAM_H + +#include <vector> +#include <boost/function.hpp> +#include <boost/type_traits/is_convertible.hpp> +#include <boost/unordered_map.hpp> +#include <boost/shared_ptr.hpp> + +#include "llerror.h" +#include "lltypeinfolookup.h" + +namespace LLInitParam +{ + // used to indicate no matching value to a given name when parsing + struct Flag{}; + + template<typename T> const T& defaultValue() { static T value; return value; } + + template <typename T, bool IS_BOOST_FUNCTION = boost::is_convertible<T, boost::function_base>::value > + struct ParamCompare + { + static bool equals(const T &a, const T &b) + { + return a == b; + } + }; + + // boost function types are not comparable + template<typename T> + struct ParamCompare<T, true> + { + static bool equals(const T&a, const T &b) + { + return false; + } + }; + + template<> + struct ParamCompare<LLSD, false> + { + static bool equals(const LLSD &a, const LLSD &b) { return false; } + }; + + template<> + struct ParamCompare<Flag, false> + { + static bool equals(const Flag& a, const Flag& b) { return false; } + }; + + + // helper functions and classes + typedef ptrdiff_t param_handle_t; + + // empty default implementation of key cache + // leverages empty base class optimization + template <typename T> + class TypeValues + { + private: + struct Inaccessable{}; + public: + typedef std::map<std::string, T> value_name_map_t; + typedef Inaccessable name_t; + + void setValueName(const std::string& key) {} + std::string getValueName() const { return ""; } + std::string calcValueName(const T& value) const { return ""; } + void clearValueName() const {} + + static bool getValueFromName(const std::string& name, T& value) + { + return false; + } + + static bool valueNamesExist() + { + return false; + } + + static std::vector<std::string>* getPossibleValues() + { + return NULL; + } + + static value_name_map_t* getValueNames() {return NULL;} + }; + + template <typename T, typename DERIVED_TYPE = TypeValues<T> > + class TypeValuesHelper + { + public: + typedef typename std::map<std::string, T> value_name_map_t; + typedef std::string name_t; + + //TODO: cache key by index to save on param block size + void setValueName(const std::string& value_name) + { + mValueName = value_name; + } + + std::string getValueName() const + { + return mValueName; + } + + std::string calcValueName(const T& value) const + { + value_name_map_t* map = getValueNames(); + for (typename value_name_map_t::iterator it = map->begin(), end_it = map->end(); + it != end_it; + ++it) + { + if (ParamCompare<T>::equals(it->second, value)) + { + return it->first; + } + } + + return ""; + } + + void clearValueName() const + { + mValueName.clear(); + } + + static bool getValueFromName(const std::string& name, T& value) + { + value_name_map_t* map = getValueNames(); + typename value_name_map_t::iterator found_it = map->find(name); + if (found_it == map->end()) return false; + + value = found_it->second; + return true; + } + + static bool valueNamesExist() + { + return !getValueNames()->empty(); + } + + static value_name_map_t* getValueNames() + { + static value_name_map_t sMap; + static bool sInitialized = false; + + if (!sInitialized) + { + sInitialized = true; + DERIVED_TYPE::declareValues(); + } + return &sMap; + } + + static std::vector<std::string>* getPossibleValues() + { + static std::vector<std::string> sValues; + + value_name_map_t* map = getValueNames(); + for (typename value_name_map_t::iterator it = map->begin(), end_it = map->end(); + it != end_it; + ++it) + { + sValues.push_back(it->first); + } + return &sValues; + } + + static void declare(const std::string& name, const T& value) + { + (*getValueNames())[name] = value; + } + + protected: + static void getName(const std::string& name, const T& value) + {} + + mutable std::string mValueName; + }; + + class LL_COMMON_API Parser + { + LOG_CLASS(Parser); + + public: + + struct CompareTypeID + { + bool operator()(const std::type_info* lhs, const std::type_info* rhs) const + { + return lhs->before(*rhs); + } + }; + + typedef std::vector<std::pair<std::string, bool> > name_stack_t; + typedef std::pair<name_stack_t::iterator, name_stack_t::iterator> name_stack_range_t; + typedef std::vector<std::string> possible_values_t; + + typedef bool (*parser_read_func_t)(Parser& parser, void* output); + typedef bool (*parser_write_func_t)(Parser& parser, const void*, name_stack_t&); + typedef boost::function<void (name_stack_t&, S32, S32, const possible_values_t*)> parser_inspect_func_t; + + typedef LLTypeInfoLookup<parser_read_func_t> parser_read_func_map_t; + typedef LLTypeInfoLookup<parser_write_func_t> parser_write_func_map_t; + typedef LLTypeInfoLookup<parser_inspect_func_t> parser_inspect_func_map_t; + + Parser(parser_read_func_map_t& read_map, parser_write_func_map_t& write_map, parser_inspect_func_map_t& inspect_map) + : mParseSilently(false), + mParserReadFuncs(&read_map), + mParserWriteFuncs(&write_map), + mParserInspectFuncs(&inspect_map) + {} + virtual ~Parser(); + + template <typename T> bool readValue(T& param) + { + parser_read_func_map_t::iterator found_it = mParserReadFuncs->find(&typeid(T)); + if (found_it != mParserReadFuncs->end()) + { + return found_it->second(*this, (void*)¶m); + } + return false; + } + + template <typename T> bool writeValue(const T& param, name_stack_t& name_stack) + { + parser_write_func_map_t::iterator found_it = mParserWriteFuncs->find(&typeid(T)); + if (found_it != mParserWriteFuncs->end()) + { + return found_it->second(*this, (const void*)¶m, name_stack); + } + return false; + } + + // dispatch inspection to registered inspection functions, for each parameter in a param block + template <typename T> bool inspectValue(name_stack_t& name_stack, S32 min_count, S32 max_count, const possible_values_t* possible_values) + { + parser_inspect_func_map_t::iterator found_it = mParserInspectFuncs->find(&typeid(T)); + if (found_it != mParserInspectFuncs->end()) + { + found_it->second(name_stack, min_count, max_count, possible_values); + return true; + } + return false; + } + + virtual std::string getCurrentElementName() = 0; + virtual void parserWarning(const std::string& message); + virtual void parserError(const std::string& message); + void setParseSilently(bool silent) { mParseSilently = silent; } + + protected: + template <typename T> + void registerParserFuncs(parser_read_func_t read_func, parser_write_func_t write_func = NULL) + { + mParserReadFuncs->insert(std::make_pair(&typeid(T), read_func)); + mParserWriteFuncs->insert(std::make_pair(&typeid(T), write_func)); + } + + template <typename T> + void registerInspectFunc(parser_inspect_func_t inspect_func) + { + mParserInspectFuncs->insert(std::make_pair(&typeid(T), inspect_func)); + } + + bool mParseSilently; + + private: + parser_read_func_map_t* mParserReadFuncs; + parser_write_func_map_t* mParserWriteFuncs; + parser_inspect_func_map_t* mParserInspectFuncs; + }; + + class Param; + + // various callbacks and constraints associated with an individual param + struct LL_COMMON_API ParamDescriptor + { + struct UserData + { + virtual ~UserData() {} + }; + + typedef bool(*merge_func_t)(Param&, const Param&, bool); + typedef bool(*deserialize_func_t)(Param&, Parser&, const Parser::name_stack_range_t&, bool); + typedef void(*serialize_func_t)(const Param&, Parser&, Parser::name_stack_t&, const Param* diff_param); + typedef void(*inspect_func_t)(const Param&, Parser&, Parser::name_stack_t&, S32 min_count, S32 max_count); + typedef bool(*validation_func_t)(const Param*); + + ParamDescriptor(param_handle_t p, + merge_func_t merge_func, + deserialize_func_t deserialize_func, + serialize_func_t serialize_func, + validation_func_t validation_func, + inspect_func_t inspect_func, + S32 min_count, + S32 max_count); + + ParamDescriptor(); + ~ParamDescriptor(); + + param_handle_t mParamHandle; + merge_func_t mMergeFunc; + deserialize_func_t mDeserializeFunc; + serialize_func_t mSerializeFunc; + inspect_func_t mInspectFunc; + validation_func_t mValidationFunc; + S32 mMinCount; + S32 mMaxCount; + S32 mNumRefs; + UserData* mUserData; + }; + + typedef boost::shared_ptr<ParamDescriptor> ParamDescriptorPtr; + + // each derived Block class keeps a static data structure maintaining offsets to various params + class LL_COMMON_API BlockDescriptor + { + public: + BlockDescriptor(); + + typedef enum e_initialization_state + { + UNINITIALIZED, + INITIALIZING, + INITIALIZED + } EInitializationState; + + void aggregateBlockData(BlockDescriptor& src_block_data); + + typedef boost::unordered_map<const std::string, ParamDescriptorPtr> param_map_t; + typedef std::vector<ParamDescriptorPtr> param_list_t; + typedef std::list<ParamDescriptorPtr> all_params_list_t; + typedef std::vector<std::pair<param_handle_t, ParamDescriptor::validation_func_t> > param_validation_list_t; + + param_map_t mNamedParams; // parameters with associated names + param_list_t mUnnamedParams; // parameters with_out_ associated names + param_validation_list_t mValidationList; // parameters that must be validated + all_params_list_t mAllParams; // all parameters, owns descriptors + size_t mMaxParamOffset; + EInitializationState mInitializationState; // whether or not static block data has been initialized + class BaseBlock* mCurrentBlockPtr; // pointer to block currently being constructed + }; + + class LL_COMMON_API BaseBlock + { + public: + //TODO: implement in terms of owned_ptr + template<typename T> + class Lazy + { + public: + Lazy() + : mPtr(NULL) + {} + + ~Lazy() + { + delete mPtr; + } + + Lazy(const Lazy& other) + { + if (other.mPtr) + { + mPtr = new T(*other.mPtr); + } + else + { + mPtr = NULL; + } + } + + Lazy<T>& operator = (const Lazy<T>& other) + { + if (other.mPtr) + { + mPtr = new T(*other.mPtr); + } + else + { + mPtr = NULL; + } + return *this; + } + + bool empty() const + { + return mPtr == NULL; + } + + void set(const T& other) + { + delete mPtr; + mPtr = new T(other); + } + + const T& get() const + { + return ensureInstance(); + } + + T& get() + { + return ensureInstance(); + } + + private: + // lazily allocate an instance of T + T* ensureInstance() const + { + if (mPtr == NULL) + { + mPtr = new T(); + } + return mPtr; + } + + private: + // if you get a compilation error with this, that means you are using a forward declared struct for T + // unfortunately, the type traits we rely on don't work with forward declared typed + //static const int dummy = sizeof(T); + + mutable T* mPtr; + }; + + // "Multiple" constraint types, put here in root class to avoid ambiguity during use + struct AnyAmount + { + enum { minCount = 0 }; + enum { maxCount = U32_MAX }; + }; + + template<U32 MIN_AMOUNT> + struct AtLeast + { + enum { minCount = MIN_AMOUNT }; + enum { maxCount = U32_MAX }; + }; + + template<U32 MAX_AMOUNT> + struct AtMost + { + enum { minCount = 0 }; + enum { maxCount = MAX_AMOUNT }; + }; + + template<U32 MIN_AMOUNT, U32 MAX_AMOUNT> + struct Between + { + enum { minCount = MIN_AMOUNT }; + enum { maxCount = MAX_AMOUNT }; + }; + + template<U32 EXACT_COUNT> + struct Exactly + { + enum { minCount = EXACT_COUNT }; + enum { maxCount = EXACT_COUNT }; + }; + + // this typedef identifies derived classes as being blocks + typedef void baseblock_base_class_t; + LOG_CLASS(BaseBlock); + friend class Param; + + virtual ~BaseBlock() {} + bool submitValue(Parser::name_stack_t& name_stack, Parser& p, bool silent=false); + + param_handle_t getHandleFromParam(const Param* param) const; + bool validateBlock(bool emit_errors = true) const; + + Param* getParamFromHandle(const param_handle_t param_handle) + { + if (param_handle == 0) return NULL; + + U8* baseblock_address = reinterpret_cast<U8*>(this); + return reinterpret_cast<Param*>(baseblock_address + param_handle); + } + + const Param* getParamFromHandle(const param_handle_t param_handle) const + { + const U8* baseblock_address = reinterpret_cast<const U8*>(this); + return reinterpret_cast<const Param*>(baseblock_address + param_handle); + } + + void addSynonym(Param& param, const std::string& synonym); + + // Blocks can override this to do custom tracking of changes + virtual void paramChanged(const Param& changed_param, bool user_provided) {} + + bool deserializeBlock(Parser& p, Parser::name_stack_range_t name_stack_range, bool new_name); + void serializeBlock(Parser& p, Parser::name_stack_t& name_stack, const BaseBlock* diff_block = NULL) const; + bool inspectBlock(Parser& p, Parser::name_stack_t name_stack = Parser::name_stack_t(), S32 min_count = 0, S32 max_count = S32_MAX) const; + + virtual const BlockDescriptor& mostDerivedBlockDescriptor() const { return selfBlockDescriptor(); } + virtual BlockDescriptor& mostDerivedBlockDescriptor() { return selfBlockDescriptor(); } + + // take all provided params from other and apply to self + bool overwriteFrom(const BaseBlock& other) + { + return false; + } + + // take all provided params that are not already provided, and apply to self + bool fillFrom(const BaseBlock& other) + { + return false; + } + + static void addParam(BlockDescriptor& block_data, ParamDescriptorPtr param, const char* name); + + ParamDescriptorPtr findParamDescriptor(const Param& param); + + protected: + void init(BlockDescriptor& descriptor, BlockDescriptor& base_descriptor, size_t block_size); + + + bool mergeBlockParam(bool source_provided, bool dst_provided, BlockDescriptor& block_data, const BaseBlock& source, bool overwrite) + { + return mergeBlock(block_data, source, overwrite); + } + // take all provided params from other and apply to self + bool mergeBlock(BlockDescriptor& block_data, const BaseBlock& other, bool overwrite); + + static BlockDescriptor& selfBlockDescriptor() + { + static BlockDescriptor sBlockDescriptor; + return sBlockDescriptor; + } + + private: + const std::string& getParamName(const BlockDescriptor& block_data, const Param* paramp) const; + }; + + template<typename T> + struct ParamCompare<BaseBlock::Lazy<T>, false > + { + static bool equals(const BaseBlock::Lazy<T>& a, const BaseBlock::Lazy<T>& b) { return !a.empty() || !b.empty(); } + }; + + class LL_COMMON_API Param + { + public: + void setProvided(bool is_provided = true) + { + mIsProvided = is_provided; + enclosingBlock().paramChanged(*this, is_provided); + } + + Param& operator =(const Param& other) + { + mIsProvided = other.mIsProvided; + // don't change mEnclosingblockoffset + return *this; + } + protected: + + bool anyProvided() const { return mIsProvided; } + + Param(BaseBlock* enclosing_block); + + // store pointer to enclosing block as offset to reduce space and allow for quick copying + BaseBlock& enclosingBlock() const + { + const U8* my_addr = reinterpret_cast<const U8*>(this); + // get address of enclosing BLOCK class using stored offset to enclosing BaseBlock class + return *const_cast<BaseBlock*> + (reinterpret_cast<const BaseBlock*> + (my_addr - (ptrdiff_t)(S32)mEnclosingBlockOffset)); + } + + private: + friend class BaseBlock; + + U32 mEnclosingBlockOffset:31; + U32 mIsProvided:1; + + }; + + // these templates allow us to distinguish between template parameters + // that derive from BaseBlock and those that don't + template<typename T, typename Void = void> + struct IsBlock + { + static const bool value = false; + struct EmptyBase {}; + typedef EmptyBase base_class_t; + }; + + template<typename T> + struct IsBlock<T, typename T::baseblock_base_class_t> + { + static const bool value = true; + typedef BaseBlock base_class_t; + }; + + template<typename T> + struct IsBlock<BaseBlock::Lazy<T>, typename T::baseblock_base_class_t > + { + static const bool value = true; + typedef BaseBlock base_class_t; + }; + + template<typename T, typename NAME_VALUE_LOOKUP, bool VALUE_IS_BLOCK = IsBlock<T>::value> + class ParamValue : public NAME_VALUE_LOOKUP + { + public: + typedef const T& value_assignment_t; + typedef T value_t; + typedef ParamValue<T, NAME_VALUE_LOOKUP, VALUE_IS_BLOCK> self_t; + + ParamValue(): mValue() {} + ParamValue(value_assignment_t other) : mValue(other) {} + + void setValue(value_assignment_t val) + { + mValue = val; + } + + value_assignment_t getValue() const + { + return mValue; + } + + T& getValue() + { + return mValue; + } + + operator value_assignment_t() const + { + return mValue; + } + + value_assignment_t operator()() const + { + return mValue; + } + + void operator ()(const typename NAME_VALUE_LOOKUP::name_t& name) + { + *this = name; + } + + self_t& operator =(const typename NAME_VALUE_LOOKUP::name_t& name) + { + if (NAME_VALUE_LOOKUP::getValueFromName(name, mValue)) + { + setValueName(name); + } + + return *this; + } + + protected: + T mValue; + }; + + template<typename T, typename NAME_VALUE_LOOKUP> + class ParamValue<T, NAME_VALUE_LOOKUP, true> + : public T, + public NAME_VALUE_LOOKUP + { + public: + typedef const T& value_assignment_t; + typedef T value_t; + typedef ParamValue<T, NAME_VALUE_LOOKUP, true> self_t; + + ParamValue() + : T(), + mValidated(false) + {} + + ParamValue(value_assignment_t other) + : T(other), + mValidated(false) + {} + + void setValue(value_assignment_t val) + { + *this = val; + } + + value_assignment_t getValue() const + { + return *this; + } + + T& getValue() + { + return *this; + } + + operator value_assignment_t() const + { + return *this; + } + + value_assignment_t operator()() const + { + return *this; + } + + void operator ()(const typename NAME_VALUE_LOOKUP::name_t& name) + { + *this = name; + } + + self_t& operator =(const typename NAME_VALUE_LOOKUP::name_t& name) + { + if (NAME_VALUE_LOOKUP::getValueFromName(name, *this)) + { + setValueName(name); + } + + return *this; + } + + protected: + mutable bool mValidated; // lazy validation flag + }; + + template<typename NAME_VALUE_LOOKUP> + class ParamValue<std::string, NAME_VALUE_LOOKUP, false> + : public NAME_VALUE_LOOKUP + { + public: + typedef const std::string& value_assignment_t; + typedef std::string value_t; + typedef ParamValue<std::string, NAME_VALUE_LOOKUP, false> self_t; + + ParamValue(): mValue() {} + ParamValue(value_assignment_t other) : mValue(other) {} + + void setValue(value_assignment_t val) + { + if (NAME_VALUE_LOOKUP::getValueFromName(val, mValue)) + { + NAME_VALUE_LOOKUP::setValueName(val); + } + else + { + mValue = val; + } + } + + value_assignment_t getValue() const + { + return mValue; + } + + std::string& getValue() + { + return mValue; + } + + operator value_assignment_t() const + { + return mValue; + } + + value_assignment_t operator()() const + { + return mValue; + } + + protected: + std::string mValue; + }; + + + template<typename T, typename NAME_VALUE_LOOKUP = TypeValues<T> > + struct ParamIterator + { + typedef typename std::vector<ParamValue<T, NAME_VALUE_LOOKUP> >::const_iterator const_iterator; + typedef typename std::vector<ParamValue<T, NAME_VALUE_LOOKUP> >::iterator iterator; + }; + + // specialize for custom parsing/decomposition of specific classes + // e.g. TypedParam<LLRect> has left, top, right, bottom, etc... + template<typename T, + typename NAME_VALUE_LOOKUP = TypeValues<T>, + bool HAS_MULTIPLE_VALUES = false, + bool VALUE_IS_BLOCK = IsBlock<ParamValue<T, NAME_VALUE_LOOKUP> >::value> + class TypedParam + : public Param, + public ParamValue<T, NAME_VALUE_LOOKUP> + { + public: + typedef TypedParam<T, NAME_VALUE_LOOKUP, HAS_MULTIPLE_VALUES, VALUE_IS_BLOCK> self_t; + typedef ParamValue<T, NAME_VALUE_LOOKUP> param_value_t; + typedef typename param_value_t::value_assignment_t value_assignment_t; + typedef NAME_VALUE_LOOKUP name_value_lookup_t; + + using param_value_t::operator(); + + TypedParam(BlockDescriptor& block_descriptor, const char* name, value_assignment_t value, ParamDescriptor::validation_func_t validate_func, S32 min_count, S32 max_count) + : Param(block_descriptor.mCurrentBlockPtr) + { + if (LL_UNLIKELY(block_descriptor.mInitializationState == BlockDescriptor::INITIALIZING)) + { + ParamDescriptorPtr param_descriptor = ParamDescriptorPtr(new ParamDescriptor( + block_descriptor.mCurrentBlockPtr->getHandleFromParam(this), + &mergeWith, + &deserializeParam, + &serializeParam, + validate_func, + &inspectParam, + min_count, max_count)); + BaseBlock::addParam(block_descriptor, param_descriptor, name); + } + + setValue(value); + } + + bool isProvided() const { return Param::anyProvided(); } + + static bool deserializeParam(Param& param, Parser& parser, const Parser::name_stack_range_t& name_stack_range, bool new_name) + { + self_t& typed_param = static_cast<self_t&>(param); + // no further names in stack, attempt to parse value now + if (name_stack_range.first == name_stack_range.second) + { + if (parser.readValue(typed_param.getValue())) + { + typed_param.clearValueName(); + typed_param.setProvided(); + return true; + } + + // try to parse a known named value + if(name_value_lookup_t::valueNamesExist()) + { + // try to parse a known named value + std::string name; + if (parser.readValue(name)) + { + // try to parse a per type named value + if (name_value_lookup_t::getValueFromName(name, typed_param.getValue())) + { + typed_param.setValueName(name); + typed_param.setProvided(); + return true; + } + + } + } + } + return false; + } + + static void serializeParam(const Param& param, Parser& parser, Parser::name_stack_t& name_stack, const Param* diff_param) + { + const self_t& typed_param = static_cast<const self_t&>(param); + if (!typed_param.isProvided()) return; + + if (!name_stack.empty()) + { + name_stack.back().second = true; + } + + std::string key = typed_param.getValueName(); + + // first try to write out name of name/value pair + + if (!key.empty()) + { + if (!diff_param || !ParamCompare<std::string>::equals(static_cast<const self_t*>(diff_param)->getValueName(), key)) + { + parser.writeValue(key, name_stack); + } + } + // then try to serialize value directly + else if (!diff_param || !ParamCompare<T>::equals(typed_param.getValue(), static_cast<const self_t*>(diff_param)->getValue())) + { + if (!parser.writeValue(typed_param.getValue(), name_stack)) + { + std::string calculated_key = typed_param.calcValueName(typed_param.getValue()); + if (!diff_param || !ParamCompare<std::string>::equals(static_cast<const self_t*>(diff_param)->getValueName(), calculated_key)) + { + parser.writeValue(calculated_key, name_stack); + } + } + } + } + + static void inspectParam(const Param& param, Parser& parser, Parser::name_stack_t& name_stack, S32 min_count, S32 max_count) + { + // tell parser about our actual type + parser.inspectValue<T>(name_stack, min_count, max_count, NULL); + // then tell it about string-based alternatives ("red", "blue", etc. for LLColor4) + if (name_value_lookup_t::getPossibleValues()) + { + parser.inspectValue<std::string>(name_stack, min_count, max_count, name_value_lookup_t::getPossibleValues()); + } + } + + void set(value_assignment_t val, bool flag_as_provided = true) + { + param_value_t::clearValueName(); + setValue(val); + setProvided(flag_as_provided); + } + + self_t& operator =(const typename NAME_VALUE_LOOKUP::name_t& name) + { + return static_cast<self_t&>(param_value_t::operator =(name)); + } + + protected: + + self_t& operator =(const self_t& other) + { + param_value_t::operator =(other); + Param::operator =(other); + return *this; + } + + static bool mergeWith(Param& dst, const Param& src, bool overwrite) + { + const self_t& src_typed_param = static_cast<const self_t&>(src); + self_t& dst_typed_param = static_cast<self_t&>(dst); + + if (src_typed_param.isProvided() + && (overwrite || !dst_typed_param.isProvided())) + { + dst_typed_param.set(src_typed_param.getValue()); + return true; + } + return false; + } + }; + + // parameter that is a block + template <typename T, typename NAME_VALUE_LOOKUP> + class TypedParam<T, NAME_VALUE_LOOKUP, false, true> + : public Param, + public ParamValue<T, NAME_VALUE_LOOKUP> + { + public: + typedef ParamValue<T, NAME_VALUE_LOOKUP> param_value_t; + typedef typename param_value_t::value_assignment_t value_assignment_t; + typedef TypedParam<T, NAME_VALUE_LOOKUP, false, true> self_t; + typedef NAME_VALUE_LOOKUP name_value_lookup_t; + + using param_value_t::operator(); + + TypedParam(BlockDescriptor& block_descriptor, const char* name, value_assignment_t value, ParamDescriptor::validation_func_t validate_func, S32 min_count, S32 max_count) + : Param(block_descriptor.mCurrentBlockPtr), + param_value_t(value) + { + if (LL_UNLIKELY(block_descriptor.mInitializationState == BlockDescriptor::INITIALIZING)) + { + ParamDescriptorPtr param_descriptor = ParamDescriptorPtr(new ParamDescriptor( + block_descriptor.mCurrentBlockPtr->getHandleFromParam(this), + &mergeWith, + &deserializeParam, + &serializeParam, + validate_func, + &inspectParam, + min_count, max_count)); + BaseBlock::addParam(block_descriptor, param_descriptor, name); + } + } + + static bool deserializeParam(Param& param, Parser& parser, const Parser::name_stack_range_t& name_stack_range, bool new_name) + { + self_t& typed_param = static_cast<self_t&>(param); + // attempt to parse block... + if(typed_param.deserializeBlock(parser, name_stack_range, new_name)) + { + typed_param.clearValueName(); + typed_param.setProvided(); + return true; + } + + if(name_value_lookup_t::valueNamesExist()) + { + // try to parse a known named value + std::string name; + if (parser.readValue(name)) + { + // try to parse a per type named value + if (name_value_lookup_t::getValueFromName(name, typed_param.getValue())) + { + typed_param.setValueName(name); + typed_param.setProvided(); + return true; + } + + } + } + return false; + } + + static void serializeParam(const Param& param, Parser& parser, Parser::name_stack_t& name_stack, const Param* diff_param) + { + const self_t& typed_param = static_cast<const self_t&>(param); + if (!typed_param.isProvided()) return; + + if (!name_stack.empty()) + { + name_stack.back().second = true; + } + + std::string key = typed_param.getValueName(); + if (!key.empty()) + { + if (!parser.writeValue(key, name_stack)) + { + return; + } + } + else + { + typed_param.serializeBlock(parser, name_stack, static_cast<const self_t*>(diff_param)); + } + } + + static void inspectParam(const Param& param, Parser& parser, Parser::name_stack_t& name_stack, S32 min_count, S32 max_count) + { + // I am a param that is also a block, so just recurse into my contents + const self_t& typed_param = static_cast<const self_t&>(param); + typed_param.inspectBlock(parser, name_stack, min_count, max_count); + } + + // a param-that-is-a-block is provided when the user has set one of its child params + // *and* the block as a whole validates + bool isProvided() const + { + // only validate block when it hasn't already passed validation with current data + if (Param::anyProvided() && !param_value_t::mValidated) + { + // a sub-block is "provided" when it has been filled in enough to be valid + param_value_t::mValidated = param_value_t::validateBlock(false); + } + return Param::anyProvided() && param_value_t::mValidated; + } + + // assign block contents to this param-that-is-a-block + void set(value_assignment_t val, bool flag_as_provided = true) + { + setValue(val); + param_value_t::clearValueName(); + // force revalidation of block + // next call to isProvided() will update provision status based on validity + param_value_t::mValidated = false; + setProvided(flag_as_provided); + } + + self_t& operator =(const typename NAME_VALUE_LOOKUP::name_t& name) + { + return static_cast<self_t&>(param_value_t::operator =(name)); + } + + // propagate changed status up to enclosing block + /*virtual*/ void paramChanged(const Param& changed_param, bool user_provided) + { + param_value_t::paramChanged(changed_param, user_provided); + if (user_provided) + { + // a child param has been explicitly changed + // so *some* aspect of this block is now provided + param_value_t::mValidated = false; + setProvided(); + param_value_t::clearValueName(); + } + else + { + Param::enclosingBlock().paramChanged(*this, user_provided); + } + } + + protected: + + self_t& operator =(const self_t& other) + { + param_value_t::operator =(other); + Param::operator =(other); + return *this; + } + + static bool mergeWith(Param& dst, const Param& src, bool overwrite) + { + const self_t& src_typed_param = static_cast<const self_t&>(src); + self_t& dst_typed_param = static_cast<self_t&>(dst); + + if (src_typed_param.anyProvided()) + { + if (dst_typed_param.mergeBlockParam(src_typed_param.isProvided(), dst_typed_param.isProvided(), param_value_t::selfBlockDescriptor(), src_typed_param, overwrite)) + { + dst_typed_param.clearValueName(); + dst_typed_param.setProvided(true); + return true; + } + } + return false; + } + }; + + // container of non-block parameters + template <typename VALUE_TYPE, typename NAME_VALUE_LOOKUP> + class TypedParam<VALUE_TYPE, NAME_VALUE_LOOKUP, true, false> + : public Param + { + public: + typedef TypedParam<VALUE_TYPE, NAME_VALUE_LOOKUP, true, false> self_t; + typedef ParamValue<VALUE_TYPE, NAME_VALUE_LOOKUP> param_value_t; + typedef typename std::vector<param_value_t> container_t; + typedef const container_t& value_assignment_t; + + typedef typename param_value_t::value_t value_t; + typedef NAME_VALUE_LOOKUP name_value_lookup_t; + + TypedParam(BlockDescriptor& block_descriptor, const char* name, value_assignment_t value, ParamDescriptor::validation_func_t validate_func, S32 min_count, S32 max_count) + : Param(block_descriptor.mCurrentBlockPtr) + { + std::copy(value.begin(), value.end(), std::back_inserter(mValues)); + + if (LL_UNLIKELY(block_descriptor.mInitializationState == BlockDescriptor::INITIALIZING)) + { + ParamDescriptorPtr param_descriptor = ParamDescriptorPtr(new ParamDescriptor( + block_descriptor.mCurrentBlockPtr->getHandleFromParam(this), + &mergeWith, + &deserializeParam, + &serializeParam, + validate_func, + &inspectParam, + min_count, max_count)); + BaseBlock::addParam(block_descriptor, param_descriptor, name); + } + } + + bool isProvided() const { return Param::anyProvided(); } + + static bool deserializeParam(Param& param, Parser& parser, const Parser::name_stack_range_t& name_stack_range, bool new_name) + { + self_t& typed_param = static_cast<self_t&>(param); + value_t value; + // no further names in stack, attempt to parse value now + if (name_stack_range.first == name_stack_range.second) + { + // attempt to read value directly + if (parser.readValue(value)) + { + typed_param.add(value); + return true; + } + + // try to parse a known named value + if(name_value_lookup_t::valueNamesExist()) + { + // try to parse a known named value + std::string name; + if (parser.readValue(name)) + { + // try to parse a per type named value + if (name_value_lookup_t::getValueFromName(name, value)) + { + typed_param.add(value); + typed_param.mValues.back().setValueName(name); + return true; + } + + } + } + } + return false; + } + + static void serializeParam(const Param& param, Parser& parser, Parser::name_stack_t& name_stack, const Param* diff_param) + { + const self_t& typed_param = static_cast<const self_t&>(param); + if (!typed_param.isProvided() || name_stack.empty()) return; + + for (const_iterator it = typed_param.mValues.begin(), end_it = typed_param.mValues.end(); + it != end_it; + ++it) + { + std::string key = it->getValueName(); + name_stack.back().second = true; + + if(key.empty()) + // not parsed via name values, write out value directly + { + bool value_written = parser.writeValue(*it, name_stack); + if (!value_written) + { + std::string calculated_key = it->calcValueName(it->getValue()); + if (!parser.writeValue(calculated_key, name_stack)) + { + break; + } + } + } + else + { + if(!parser.writeValue(key, name_stack)) + { + break; + } + } + } + } + + static void inspectParam(const Param& param, Parser& parser, Parser::name_stack_t& name_stack, S32 min_count, S32 max_count) + { + parser.inspectValue<VALUE_TYPE>(name_stack, min_count, max_count, NULL); + if (name_value_lookup_t::getPossibleValues()) + { + parser.inspectValue<std::string>(name_stack, min_count, max_count, name_value_lookup_t::getPossibleValues()); + } + } + + void set(value_assignment_t val, bool flag_as_provided = true) + { + mValues = val; + setProvided(flag_as_provided); + } + + param_value_t& add() + { + mValues.push_back(param_value_t(value_t())); + Param::setProvided(); + return mValues.back(); + } + + self_t& add(const value_t& item) + { + param_value_t param_value; + param_value.setValue(item); + mValues.push_back(param_value); + setProvided(); + return *this; + } + + self_t& add(const typename name_value_lookup_t::name_t& name) + { + value_t value; + + // try to parse a per type named value + if (name_value_lookup_t::getValueFromName(name, value)) + { + add(value); + mValues.back().setValueName(name); + } + + return *this; + } + + // implicit conversion + operator value_assignment_t() const { return mValues; } + // explicit conversion + value_assignment_t operator()() const { return mValues; } + + typedef typename container_t::iterator iterator; + typedef typename container_t::const_iterator const_iterator; + iterator begin() { return mValues.begin(); } + iterator end() { return mValues.end(); } + const_iterator begin() const { return mValues.begin(); } + const_iterator end() const { return mValues.end(); } + bool empty() const { return mValues.empty(); } + size_t size() const { return mValues.size(); } + + U32 numValidElements() const + { + return mValues.size(); + } + + protected: + static bool mergeWith(Param& dst, const Param& src, bool overwrite) + { + const self_t& src_typed_param = static_cast<const self_t&>(src); + self_t& dst_typed_param = static_cast<self_t&>(dst); + + if (overwrite) + { + std::copy(src_typed_param.begin(), src_typed_param.end(), std::back_inserter(dst_typed_param.mValues)); + } + else + { + container_t new_values(src_typed_param.mValues); + std::copy(dst_typed_param.begin(), dst_typed_param.end(), std::back_inserter(new_values)); + std::swap(dst_typed_param.mValues, new_values); + } + + if (src_typed_param.begin() != src_typed_param.end()) + { + dst_typed_param.setProvided(); + } + return true; + } + + container_t mValues; + }; + + // container of block parameters + template <typename VALUE_TYPE, typename NAME_VALUE_LOOKUP> + class TypedParam<VALUE_TYPE, NAME_VALUE_LOOKUP, true, true> + : public Param + { + public: + typedef TypedParam<VALUE_TYPE, NAME_VALUE_LOOKUP, true, true> self_t; + typedef ParamValue<VALUE_TYPE, NAME_VALUE_LOOKUP> param_value_t; + typedef typename std::vector<param_value_t> container_t; + typedef const container_t& value_assignment_t; + typedef typename param_value_t::value_t value_t; + typedef NAME_VALUE_LOOKUP name_value_lookup_t; + + TypedParam(BlockDescriptor& block_descriptor, const char* name, value_assignment_t value, ParamDescriptor::validation_func_t validate_func, S32 min_count, S32 max_count) + : Param(block_descriptor.mCurrentBlockPtr) + { + std::copy(value.begin(), value.end(), back_inserter(mValues)); + + if (LL_UNLIKELY(block_descriptor.mInitializationState == BlockDescriptor::INITIALIZING)) + { + ParamDescriptorPtr param_descriptor = ParamDescriptorPtr(new ParamDescriptor( + block_descriptor.mCurrentBlockPtr->getHandleFromParam(this), + &mergeWith, + &deserializeParam, + &serializeParam, + validate_func, + &inspectParam, + min_count, max_count)); + BaseBlock::addParam(block_descriptor, param_descriptor, name); + } + } + + bool isProvided() const { return Param::anyProvided(); } + + static bool deserializeParam(Param& param, Parser& parser, const Parser::name_stack_range_t& name_stack_range, bool new_name) + { + self_t& typed_param = static_cast<self_t&>(param); + bool new_value = false; + + if (new_name || typed_param.mValues.empty()) + { + new_value = true; + typed_param.mValues.push_back(value_t()); + } + + param_value_t& value = typed_param.mValues.back(); + + // attempt to parse block... + if(value.deserializeBlock(parser, name_stack_range, new_name)) + { + typed_param.setProvided(); + return true; + } + else if(name_value_lookup_t::valueNamesExist()) + { + // try to parse a known named value + std::string name; + if (parser.readValue(name)) + { + // try to parse a per type named value + if (name_value_lookup_t::getValueFromName(name, value.getValue())) + { + typed_param.mValues.back().setValueName(name); + typed_param.setProvided(); + return true; + } + + } + } + + if (new_value) + { // failed to parse new value, pop it off + typed_param.mValues.pop_back(); + } + + return false; + } + + static void serializeParam(const Param& param, Parser& parser, Parser::name_stack_t& name_stack, const Param* diff_param) + { + const self_t& typed_param = static_cast<const self_t&>(param); + if (!typed_param.isProvided() || name_stack.empty()) return; + + for (const_iterator it = typed_param.mValues.begin(), end_it = typed_param.mValues.end(); + it != end_it; + ++it) + { + name_stack.back().second = true; + + std::string key = it->getValueName(); + if (!key.empty()) + { + parser.writeValue(key, name_stack); + } + // Not parsed via named values, write out value directly + // NOTE: currently we don't worry about removing default values in Multiple + else + { + it->serializeBlock(parser, name_stack, NULL); + } + } + } + + static void inspectParam(const Param& param, Parser& parser, Parser::name_stack_t& name_stack, S32 min_count, S32 max_count) + { + // I am a vector of blocks, so describe my contents recursively + param_value_t(value_t()).inspectBlock(parser, name_stack, min_count, max_count); + } + + void set(value_assignment_t val, bool flag_as_provided = true) + { + mValues = val; + setProvided(flag_as_provided); + } + + param_value_t& add() + { + mValues.push_back(value_t()); + setProvided(); + return mValues.back(); + } + + self_t& add(const value_t& item) + { + mValues.push_back(item); + setProvided(); + return *this; + } + + self_t& add(const typename name_value_lookup_t::name_t& name) + { + value_t value; + + // try to parse a per type named value + if (name_value_lookup_t::getValueFromName(name, value)) + { + add(value); + mValues.back().setValueName(name); + } + return *this; + } + + // implicit conversion + operator value_assignment_t() const { return mValues; } + // explicit conversion + value_assignment_t operator()() const { return mValues; } + + typedef typename container_t::iterator iterator; + typedef typename container_t::const_iterator const_iterator; + iterator begin() { return mValues.begin(); } + iterator end() { return mValues.end(); } + const_iterator begin() const { return mValues.begin(); } + const_iterator end() const { return mValues.end(); } + bool empty() const { return mValues.empty(); } + size_t size() const { return mValues.size(); } + + U32 numValidElements() const + { + U32 count = 0; + for (const_iterator it = mValues.begin(), end_it = mValues.end(); + it != end_it; + ++it) + { + if(it->validateBlock(false)) count++; + } + return count; + } + + protected: + + static bool mergeWith(Param& dst, const Param& src, bool overwrite) + { + const self_t& src_typed_param = static_cast<const self_t&>(src); + self_t& dst_typed_param = static_cast<self_t&>(dst); + + if (overwrite) + { + std::copy(src_typed_param.begin(), src_typed_param.end(), std::back_inserter(dst_typed_param.mValues)); + } + else + { + container_t new_values(src_typed_param.mValues); + std::copy(dst_typed_param.begin(), dst_typed_param.end(), std::back_inserter(new_values)); + std::swap(dst_typed_param.mValues, new_values); + } + + if (src_typed_param.begin() != src_typed_param.end()) + { + dst_typed_param.setProvided(); + } + + return true; + } + + container_t mValues; + }; + + template <typename DERIVED_BLOCK, typename BASE_BLOCK = BaseBlock> + class ChoiceBlock : public BASE_BLOCK + { + typedef ChoiceBlock<DERIVED_BLOCK, BASE_BLOCK> self_t; + typedef ChoiceBlock<DERIVED_BLOCK, BASE_BLOCK> enclosing_block_t; + typedef BASE_BLOCK base_block_t; + + LOG_CLASS(self_t); + public: + // take all provided params from other and apply to self + bool overwriteFrom(const self_t& other) + { + return static_cast<DERIVED_BLOCK*>(this)->mergeBlock(selfBlockDescriptor(), other, true); + } + + // take all provided params that are not already provided, and apply to self + bool fillFrom(const self_t& other) + { + return static_cast<DERIVED_BLOCK*>(this)->mergeBlock(selfBlockDescriptor(), other, false); + } + + bool mergeBlockParam(bool source_provided, bool dest_provided, BlockDescriptor& block_data, const self_t& source, bool overwrite) + { + bool source_override = source_provided && (overwrite || !dest_provided); + + if (source_override || source.mCurChoice == mCurChoice) + { + return mergeBlock(block_data, source, overwrite); + } + return false; + } + + // merge with other block + bool mergeBlock(BlockDescriptor& block_data, const self_t& other, bool overwrite) + { + mCurChoice = other.mCurChoice; + return base_block_t::mergeBlock(selfBlockDescriptor(), other, overwrite); + } + + // clear out old choice when param has changed + /*virtual*/ void paramChanged(const Param& changed_param, bool user_provided) + { + param_handle_t changed_param_handle = base_block_t::getHandleFromParam(&changed_param); + // if we have a new choice... + if (changed_param_handle != mCurChoice) + { + // clear provided flag on previous choice + Param* previous_choice = base_block_t::getParamFromHandle(mCurChoice); + if (previous_choice) + { + previous_choice->setProvided(false); + } + mCurChoice = changed_param_handle; + } + base_block_t::paramChanged(changed_param, user_provided); + } + + virtual const BlockDescriptor& mostDerivedBlockDescriptor() const { return selfBlockDescriptor(); } + virtual BlockDescriptor& mostDerivedBlockDescriptor() { return selfBlockDescriptor(); } + + protected: + ChoiceBlock() + : mCurChoice(0) + { + BaseBlock::init(selfBlockDescriptor(), base_block_t::selfBlockDescriptor(), sizeof(DERIVED_BLOCK)); + } + + // Alternatives are mutually exclusive wrt other Alternatives in the same block. + // One alternative in a block will always have isChosen() == true. + // At most one alternative in a block will have isProvided() == true. + template <typename T, typename NAME_VALUE_LOOKUP = TypeValues<T> > + class Alternative : public TypedParam<T, NAME_VALUE_LOOKUP, false> + { + public: + friend class ChoiceBlock<DERIVED_BLOCK>; + + typedef Alternative<T, NAME_VALUE_LOOKUP> self_t; + typedef TypedParam<T, NAME_VALUE_LOOKUP, false, IsBlock<ParamValue<T, NAME_VALUE_LOOKUP> >::value> super_t; + typedef typename super_t::value_assignment_t value_assignment_t; + + using super_t::operator =; + + explicit Alternative(const char* name = "", value_assignment_t val = defaultValue<T>()) + : super_t(DERIVED_BLOCK::selfBlockDescriptor(), name, val, NULL, 0, 1), + mOriginalValue(val) + { + // assign initial choice to first declared option + DERIVED_BLOCK* blockp = ((DERIVED_BLOCK*)DERIVED_BLOCK::selfBlockDescriptor().mCurrentBlockPtr); + if (LL_UNLIKELY(DERIVED_BLOCK::selfBlockDescriptor().mInitializationState == BlockDescriptor::INITIALIZING)) + { + if(blockp->mCurChoice == 0) + { + blockp->mCurChoice = Param::enclosingBlock().getHandleFromParam(this); + } + } + } + + void choose() + { + static_cast<enclosing_block_t&>(Param::enclosingBlock()).paramChanged(*this, true); + } + + void chooseAs(value_assignment_t val) + { + super_t::set(val); + } + + void operator =(value_assignment_t val) + { + super_t::set(val); + } + + void operator()(typename super_t::value_assignment_t val) + { + super_t::set(val); + } + + operator value_assignment_t() const + { + return (*this)(); + } + + value_assignment_t operator()() const + { + if (static_cast<enclosing_block_t&>(Param::enclosingBlock()).getCurrentChoice() == this) + { + return super_t::getValue(); + } + return mOriginalValue; + } + + bool isChosen() const + { + return static_cast<enclosing_block_t&>(Param::enclosingBlock()).getCurrentChoice() == this; + } + + private: + T mOriginalValue; + }; + + protected: + static BlockDescriptor& selfBlockDescriptor() + { + static BlockDescriptor sBlockDescriptor; + return sBlockDescriptor; + } + + private: + param_handle_t mCurChoice; + + const Param* getCurrentChoice() const + { + return base_block_t::getParamFromHandle(mCurChoice); + } + }; + + template <typename DERIVED_BLOCK, typename BASE_BLOCK = BaseBlock> + class Block + : public BASE_BLOCK + { + typedef Block<DERIVED_BLOCK, BASE_BLOCK> self_t; + typedef Block<DERIVED_BLOCK, BASE_BLOCK> block_t; + + public: + typedef BASE_BLOCK base_block_t; + + // take all provided params from other and apply to self + bool overwriteFrom(const self_t& other) + { + return static_cast<DERIVED_BLOCK*>(this)->mergeBlock(selfBlockDescriptor(), other, true); + } + + // take all provided params that are not already provided, and apply to self + bool fillFrom(const self_t& other) + { + return static_cast<DERIVED_BLOCK*>(this)->mergeBlock(selfBlockDescriptor(), other, false); + } + + virtual const BlockDescriptor& mostDerivedBlockDescriptor() const { return selfBlockDescriptor(); } + virtual BlockDescriptor& mostDerivedBlockDescriptor() { return selfBlockDescriptor(); } + + protected: + Block() + { + //#pragma message("Parsing LLInitParam::Block") + BaseBlock::init(selfBlockDescriptor(), BASE_BLOCK::selfBlockDescriptor(), sizeof(DERIVED_BLOCK)); + } + + // + // Nested classes for declaring parameters + // + template <typename T, typename NAME_VALUE_LOOKUP = TypeValues<T> > + class Optional : public TypedParam<T, NAME_VALUE_LOOKUP, false> + { + public: + typedef TypedParam<T, NAME_VALUE_LOOKUP, false, IsBlock<ParamValue<T, NAME_VALUE_LOOKUP> >::value> super_t; + typedef typename super_t::value_assignment_t value_assignment_t; + + using super_t::operator(); + using super_t::operator =; + + explicit Optional(const char* name = "", value_assignment_t val = defaultValue<T>()) + : super_t(DERIVED_BLOCK::selfBlockDescriptor(), name, val, NULL, 0, 1) + { + //#pragma message("Parsing LLInitParam::Block::Optional") + } + + Optional& operator =(value_assignment_t val) + { + set(val); + return *this; + } + + DERIVED_BLOCK& operator()(value_assignment_t val) + { + super_t::set(val); + return static_cast<DERIVED_BLOCK&>(Param::enclosingBlock()); + } + }; + + template <typename T, typename NAME_VALUE_LOOKUP = TypeValues<T> > + class Mandatory : public TypedParam<T, NAME_VALUE_LOOKUP, false> + { + public: + typedef TypedParam<T, NAME_VALUE_LOOKUP, false, IsBlock<ParamValue<T, NAME_VALUE_LOOKUP> >::value> super_t; + typedef Mandatory<T, NAME_VALUE_LOOKUP> self_t; + typedef typename super_t::value_assignment_t value_assignment_t; + + using super_t::operator(); + using super_t::operator =; + + // mandatory parameters require a name to be parseable + explicit Mandatory(const char* name = "", value_assignment_t val = defaultValue<T>()) + : super_t(DERIVED_BLOCK::selfBlockDescriptor(), name, val, &validate, 1, 1) + {} + + Mandatory& operator =(value_assignment_t val) + { + set(val); + return *this; + } + + DERIVED_BLOCK& operator()(typename super_t::value_assignment_t val) + { + super_t::set(val); + return static_cast<DERIVED_BLOCK&>(Param::enclosingBlock()); + } + + static bool validate(const Param* p) + { + // valid only if provided + return static_cast<const self_t*>(p)->isProvided(); + } + + }; + + template <typename T, typename RANGE = BaseBlock::AnyAmount, typename NAME_VALUE_LOOKUP = TypeValues<T> > + class Multiple : public TypedParam<T, NAME_VALUE_LOOKUP, true> + { + public: + typedef TypedParam<T, NAME_VALUE_LOOKUP, true, IsBlock<ParamValue<T, NAME_VALUE_LOOKUP> >::value> super_t; + typedef Multiple<T, RANGE, NAME_VALUE_LOOKUP> self_t; + typedef typename super_t::container_t container_t; + typedef typename super_t::value_assignment_t value_assignment_t; + typedef typename super_t::iterator iterator; + typedef typename super_t::const_iterator const_iterator; + + explicit Multiple(const char* name = "") + : super_t(DERIVED_BLOCK::selfBlockDescriptor(), name, container_t(), &validate, RANGE::minCount, RANGE::maxCount) + {} + + Multiple& operator =(value_assignment_t val) + { + set(val); + return *this; + } + + DERIVED_BLOCK& operator()(typename super_t::value_assignment_t val) + { + super_t::set(val); + return static_cast<DERIVED_BLOCK&>(Param::enclosingBlock()); + } + + static bool validate(const Param* paramp) + { + U32 num_valid = ((super_t*)paramp)->numValidElements(); + return RANGE::minCount <= num_valid && num_valid <= RANGE::maxCount; + } + }; + + class Deprecated : public Param + { + public: + explicit Deprecated(const char* name) + : Param(DERIVED_BLOCK::selfBlockDescriptor().mCurrentBlockPtr) + { + BlockDescriptor& block_descriptor = DERIVED_BLOCK::selfBlockDescriptor(); + if (LL_UNLIKELY(block_descriptor.mInitializationState == BlockDescriptor::INITIALIZING)) + { + ParamDescriptorPtr param_descriptor = ParamDescriptorPtr(new ParamDescriptor( + block_descriptor.mCurrentBlockPtr->getHandleFromParam(this), + NULL, + &deserializeParam, + NULL, + NULL, + NULL, + 0, S32_MAX)); + BaseBlock::addParam(block_descriptor, param_descriptor, name); + } + } + + static bool deserializeParam(Param& param, Parser& parser, const Parser::name_stack_range_t& name_stack_range, bool new_name) + { + if (name_stack_range.first == name_stack_range.second) + { + //std::string message = llformat("Deprecated value %s ignored", getName().c_str()); + //parser.parserWarning(message); + return true; + } + + return false; + } + }; + + // different semantics for documentation purposes, but functionally identical + typedef Deprecated Ignored; + + protected: + static BlockDescriptor& selfBlockDescriptor() + { + static BlockDescriptor sBlockDescriptor; + return sBlockDescriptor; + } + + template <typename T, typename NAME_VALUE_LOOKUP, bool multiple, bool is_block> + void changeDefault(TypedParam<T, NAME_VALUE_LOOKUP, multiple, is_block>& param, + typename TypedParam<T, NAME_VALUE_LOOKUP, multiple, is_block>::value_assignment_t value) + { + if (!param.isProvided()) + { + param.set(value, false); + } + } + + }; + + template <typename DERIVED_BLOCK, typename BASE_BLOCK = BaseBlock> + class BatchBlock + : public Block<DERIVED_BLOCK, BASE_BLOCK> + { + public: + typedef BatchBlock<DERIVED_BLOCK, BASE_BLOCK> self_t; + typedef Block<DERIVED_BLOCK, BASE_BLOCK> super_t; + + BatchBlock() + {} + + bool deserializeBlock(Parser& p, Parser::name_stack_range_t name_stack_range, bool new_name) + { + if (new_name) + { + // reset block + *static_cast<DERIVED_BLOCK*>(this) = defaultBatchValue(); + } + return super_t::deserializeBlock(p, name_stack_range, new_name); + } + + bool mergeBlock(BlockDescriptor& block_data, const BaseBlock& other, bool overwrite) + { + if (overwrite) + { + *static_cast<DERIVED_BLOCK*>(this) = defaultBatchValue(); + // merge individual parameters into destination + return super_t::mergeBlock(super_t::selfBlockDescriptor(), other, overwrite); + } + return false; + } + protected: + static const DERIVED_BLOCK& defaultBatchValue() + { + static DERIVED_BLOCK default_value; + return default_value; + } + }; + + // FIXME: this specialization is not currently used, as it only matches against the BatchBlock base class + // and not the derived class with the actual params + template<typename DERIVED_BLOCK, + typename BASE_BLOCK, + typename NAME_VALUE_LOOKUP> + class ParamValue <BatchBlock<DERIVED_BLOCK, BASE_BLOCK>, + NAME_VALUE_LOOKUP, + true> + : public NAME_VALUE_LOOKUP, + protected BatchBlock<DERIVED_BLOCK, BASE_BLOCK> + { + public: + typedef BatchBlock<DERIVED_BLOCK, BASE_BLOCK> block_t; + typedef const BatchBlock<DERIVED_BLOCK, BASE_BLOCK>& value_assignment_t; + typedef block_t value_t; + + ParamValue() + : block_t(), + mValidated(false) + {} + + ParamValue(value_assignment_t other) + : block_t(other), + mValidated(false) + { + } + + void setValue(value_assignment_t val) + { + *this = val; + } + + value_assignment_t getValue() const + { + return *this; + } + + BatchBlock<DERIVED_BLOCK, BASE_BLOCK>& getValue() + { + return *this; + } + + operator value_assignment_t() const + { + return *this; + } + + value_assignment_t operator()() const + { + return *this; + } + + protected: + mutable bool mValidated; // lazy validation flag + }; + + template<typename T, bool IS_BLOCK> + class ParamValue <BaseBlock::Lazy<T>, + TypeValues<T>, + IS_BLOCK> + : public IsBlock<T>::base_class_t + { + public: + typedef ParamValue <BaseBlock::Lazy<T>, TypeValues<T>, false> self_t; + typedef const T& value_assignment_t; + typedef T value_t; + + ParamValue() + : mValue(), + mValidated(false) + {} + + ParamValue(value_assignment_t other) + : mValue(other), + mValidated(false) + {} + + void setValue(value_assignment_t val) + { + mValue.set(val); + } + + value_assignment_t getValue() const + { + return mValue.get(); + } + + T& getValue() + { + return mValue.get(); + } + + operator value_assignment_t() const + { + return mValue.get(); + } + + value_assignment_t operator()() const + { + return mValue.get(); + } + + bool deserializeBlock(Parser& p, Parser::name_stack_range_t name_stack_range, bool new_name) + { + return mValue.get().deserializeBlock(p, name_stack_range, new_name); + } + + void serializeBlock(Parser& p, Parser::name_stack_t& name_stack, const BaseBlock* diff_block = NULL) const + { + if (mValue.empty()) return; + + mValue.get().serializeBlock(p, name_stack, diff_block); + } + + bool inspectBlock(Parser& p, Parser::name_stack_t name_stack = Parser::name_stack_t(), S32 min_count = 0, S32 max_count = S32_MAX) const + { + if (mValue.empty()) return false; + + return mValue.get().inspectBlock(p, name_stack, min_count, max_count); + } + + protected: + mutable bool mValidated; // lazy validation flag + + private: + BaseBlock::Lazy<T> mValue; + }; + + template <> + class ParamValue <LLSD, + TypeValues<LLSD>, + false> + : public TypeValues<LLSD>, + public BaseBlock + { + public: + typedef ParamValue<LLSD, TypeValues<LLSD>, false> self_t; + typedef const LLSD& value_assignment_t; + + ParamValue() + : mValidated(false) + {} + + ParamValue(value_assignment_t other) + : mValue(other), + mValidated(false) + {} + + void setValue(value_assignment_t val) { mValue = val; } + + value_assignment_t getValue() const { return mValue; } + LLSD& getValue() { return mValue; } + + operator value_assignment_t() const { return mValue; } + value_assignment_t operator()() const { return mValue; } + + + // block param interface + LL_COMMON_API bool deserializeBlock(Parser& p, Parser::name_stack_range_t name_stack_range, bool new_name); + LL_COMMON_API void serializeBlock(Parser& p, Parser::name_stack_t& name_stack, const BaseBlock* diff_block = NULL) const; + bool inspectBlock(Parser& p, Parser::name_stack_t name_stack = Parser::name_stack_t(), S32 min_count = 0, S32 max_count = S32_MAX) const + { + //TODO: implement LLSD params as schema type Any + return true; + } + + protected: + mutable bool mValidated; // lazy validation flag + + private: + static void serializeElement(Parser& p, const LLSD& sd, Parser::name_stack_t& name_stack); + + LLSD mValue; + }; + + template<typename T> + class CustomParamValue + : public Block<ParamValue<T, TypeValues<T> > >, + public TypeValues<T> + { + public: + typedef enum e_value_age + { + VALUE_NEEDS_UPDATE, // mValue needs to be refreshed from the block parameters + VALUE_AUTHORITATIVE, // mValue holds the authoritative value (which has been replicated to the block parameters via updateBlockFromValue) + BLOCK_AUTHORITATIVE // mValue is derived from the block parameters, which are authoritative + } EValueAge; + + typedef ParamValue<T, TypeValues<T> > derived_t; + typedef CustomParamValue<T> self_t; + typedef Block<derived_t> block_t; + typedef const T& value_assignment_t; + typedef T value_t; + + + CustomParamValue(const T& value = T()) + : mValue(value), + mValueAge(VALUE_AUTHORITATIVE), + mValidated(false) + {} + + bool deserializeBlock(Parser& parser, Parser::name_stack_range_t name_stack_range, bool new_name) + { + derived_t& typed_param = static_cast<derived_t&>(*this); + // try to parse direct value T + if (name_stack_range.first == name_stack_range.second) + { + if(parser.readValue(typed_param.mValue)) + { + typed_param.mValueAge = VALUE_AUTHORITATIVE; + typed_param.updateBlockFromValue(false); + + typed_param.clearValueName(); + + return true; + } + } + + // fall back on parsing block components for T + return typed_param.BaseBlock::deserializeBlock(parser, name_stack_range, new_name); + } + + void serializeBlock(Parser& parser, Parser::name_stack_t& name_stack, const BaseBlock* diff_block = NULL) const + { + const derived_t& typed_param = static_cast<const derived_t&>(*this); + const derived_t* diff_param = static_cast<const derived_t*>(diff_block); + + std::string key = typed_param.getValueName(); + + // first try to write out name of name/value pair + if (!key.empty()) + { + if (!diff_param || !ParamCompare<std::string>::equals(diff_param->getValueName(), key)) + { + parser.writeValue(key, name_stack); + } + } + // then try to serialize value directly + else if (!diff_param || !ParamCompare<T>::equals(typed_param.getValue(), diff_param->getValue())) + { + + if (!parser.writeValue(typed_param.getValue(), name_stack)) + { + //RN: *always* serialize provided components of BlockValue (don't pass diff_param on), + // since these tend to be viewed as the constructor arguments for the value T. It seems + // cleaner to treat the uniqueness of a BlockValue according to the generated value, and + // not the individual components. This way <color red="0" green="1" blue="0"/> will not + // be exported as <color green="1"/>, since it was probably the intent of the user to + // be specific about the RGB color values. This also fixes an issue where we distinguish + // between rect.left not being provided and rect.left being explicitly set to 0 (same as default) + + if (typed_param.mValueAge == VALUE_AUTHORITATIVE) + { + // if the value is authoritative but the parser doesn't accept the value type + // go ahead and make a copy, and splat the value out to its component params + // and serialize those params + derived_t copy(typed_param); + copy.updateBlockFromValue(true); + copy.block_t::serializeBlock(parser, name_stack, NULL); + } + else + { + block_t::serializeBlock(parser, name_stack, NULL); + } + } + } + } + + bool inspectBlock(Parser& parser, Parser::name_stack_t name_stack = Parser::name_stack_t(), S32 min_count = 0, S32 max_count = S32_MAX) const + { + // first, inspect with actual type... + parser.inspectValue<T>(name_stack, min_count, max_count, NULL); + if (TypeValues<T>::getPossibleValues()) + { + //...then inspect with possible string values... + parser.inspectValue<std::string>(name_stack, min_count, max_count, TypeValues<T>::getPossibleValues()); + } + // then recursively inspect contents... + return block_t::inspectBlock(parser, name_stack, min_count, max_count); + } + + bool validateBlock(bool emit_errors = true) const + { + if (mValueAge == VALUE_NEEDS_UPDATE) + { + if (block_t::validateBlock(emit_errors)) + { + // clear stale keyword associated with old value + TypeValues<T>::clearValueName(); + mValueAge = BLOCK_AUTHORITATIVE; + static_cast<derived_t*>(const_cast<self_t*>(this))->updateValueFromBlock(); + return true; + } + else + { + //block value incomplete, so not considered provided + // will attempt to revalidate on next call to isProvided() + return false; + } + } + else + { + // we have a valid value in hand + return true; + } + } + + // propagate change status up to enclosing block + /*virtual*/ void paramChanged(const Param& changed_param, bool user_provided) + { + BaseBlock::paramChanged(changed_param, user_provided); + if (user_provided) + { + // a parameter changed, so our value is out of date + mValueAge = VALUE_NEEDS_UPDATE; + } + } + + void setValue(value_assignment_t val) + { + derived_t& typed_param = static_cast<derived_t&>(*this); + // set param version number to be up to date, so we ignore block contents + mValueAge = VALUE_AUTHORITATIVE; + mValue = val; + typed_param.clearValueName(); + static_cast<derived_t*>(this)->updateBlockFromValue(false); + } + + value_assignment_t getValue() const + { + validateBlock(true); + return mValue; + } + + T& getValue() + { + validateBlock(true); + return mValue; + } + + operator value_assignment_t() const + { + return getValue(); + } + + value_assignment_t operator()() const + { + return getValue(); + } + + protected: + + // use this from within updateValueFromBlock() to set the value without making it authoritative + void updateValue(value_assignment_t value) + { + mValue = value; + } + + bool mergeBlockParam(bool source_provided, bool dst_provided, BlockDescriptor& block_data, const BaseBlock& source, bool overwrite) + { + bool source_override = source_provided && (overwrite || !dst_provided); + + const derived_t& src_typed_param = static_cast<const derived_t&>(source); + + if (source_override && src_typed_param.mValueAge == VALUE_AUTHORITATIVE) + { + // copy value over + setValue(src_typed_param.getValue()); + return true; + } + // merge individual parameters into destination + if (mValueAge == VALUE_AUTHORITATIVE) + { + static_cast<derived_t*>(this)->updateBlockFromValue(dst_provided); + } + return mergeBlock(block_data, source, overwrite); + } + + bool mergeBlock(BlockDescriptor& block_data, const BaseBlock& source, bool overwrite) + { + return block_t::mergeBlock(block_data, source, overwrite); + } + + mutable bool mValidated; // lazy validation flag + + private: + mutable T mValue; + mutable EValueAge mValueAge; + }; +} + + +#endif // LL_LLPARAM_H 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..715df36f39 --- /dev/null +++ b/indra/llcommon/llprocess.cpp @@ -0,0 +1,1351 @@ +/** + * @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. + size_type readlen((std::min)(size(), len)); + // Formally, &buffer[0] is invalid for a vector of size() 0. Exit + // early in that situation. + if (! readlen) + return ""; + // Make a buffer big enough. + 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)); + + // IQA-490, CHOP-900: On Windows, ask APR to jump through hoops to + // constrain the set of handles passed to the child process. Before we + // changed to APR, the Windows implementation of LLProcessLauncher called + // CreateProcess(bInheritHandles=FALSE), meaning to pass NO open handles + // to the child process. Now that we support pipes, though, we must allow + // apr_proc_create() to pass bInheritHandles=TRUE. But without taking + // special pains, that causes trouble in a number of ways, due to the fact + // that the viewer is constantly opening and closing files -- most of + // which CreateProcess() passes to every child process! +#if ! defined(APR_HAS_PROCATTR_CONSTRAIN_HANDLE_SET) + // Our special preprocessor symbol isn't even defined -- wrong APR + LL_WARNS("LLProcess") << "This version of APR lacks Linden " + << "apr_procattr_constrain_handle_set() extension" << LL_ENDL; +#else + chkapr(apr_procattr_constrain_handle_set(procattr, 1)); +#endif + + // 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) + // Our special preprocessor symbol isn't even defined -- wrong APR + LL_WARNS("LLProcess") << "This version of APR lacks Linden apr_procattr_autokill_set() extension" << LL_ENDL; +#elif ! APR_HAS_PROCATTR_AUTOKILL_SET + // Symbol is defined, but to 0: expect apr_procattr_autokill_set() to + // return APR_ENOTIMPL. +#else // APR_HAS_PROCATTR_AUTOKILL_SET nonzero + ll_apr_warn_status(apr_procattr_autokill_set(procattr, 1)); +#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(); +} + +//static +bool LLProcess::kill(const LLProcessPtr& p, const std::string& who) +{ + if (! p) + return true; // process dead! (was never running) + return p->kill(who); +} + +bool LLProcess::isRunning() const +{ + return getStatus().mState == RUNNING; +} + +//static +bool LLProcess::isRunning(const LLProcessPtr& p) +{ + if (! p) + return false; + return p->isRunning(); +} + +LLProcess::Status LLProcess::getStatus() const +{ + return mStatus; +} + +//static +LLProcess::Status LLProcess::getStatus(const LLProcessPtr& p) +{ + if (! p) + { + // default-constructed Status has mState == UNSTARTED + return Status(); + } + return p->getStatus(); +} + +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 LLProcessPtr& p) +{ + if (! p) + { + // default-constructed Status has mState == UNSTARTED + return getStatusString(desc, Status()); + } + return desc + " " + p->getStatusString(); +} + +//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..d711ce2f74 --- /dev/null +++ b/indra/llcommon/llprocess.h @@ -0,0 +1,553 @@ +/** + * @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; + // static isRunning(LLProcessPtr), getStatus(LLProcessPtr), + // getStatusString(LLProcessPtr), kill(LLProcessPtr) handle the case in + // which the passed LLProcessPtr might be NULL (default-constructed). + static bool isRunning(const LLProcessPtr&); + + /** + * 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; + static Status getStatus(const LLProcessPtr&); + /// English Status string query, for logging etc. + std::string getStatusString() const; + static std::string getStatusString(const std::string& desc, const LLProcessPtr&); + /// 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=""); + static bool kill(const LLProcessPtr& p, 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 this 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. This 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/llregistry.h b/indra/llcommon/llregistry.h new file mode 100644 index 0000000000..36d7f7a44c --- /dev/null +++ b/indra/llcommon/llregistry.h @@ -0,0 +1,370 @@ +/** + * @file llregistry.h + * @brief template classes for registering name, value pairs in nested scopes, statically, etc. + * + * $LicenseInfo:firstyear=2001&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_LLREGISTRY_H +#define LL_LLREGISTRY_H + +#include <list> + +#include <boost/type_traits.hpp> +#include "llsingleton.h" +#include "lltypeinfolookup.h" + +template <typename T> +class LLRegistryDefaultComparator +{ + bool operator()(const T& lhs, const T& rhs) { return lhs < rhs; } +}; + +template <typename KEY, typename VALUE> +struct LLRegistryMapSelector +{ + typedef std::map<KEY, VALUE> type; +}; + +template <typename VALUE> +struct LLRegistryMapSelector<std::type_info*, VALUE> +{ + typedef LLTypeInfoLookup<VALUE> type; +}; + +template <typename VALUE> +struct LLRegistryMapSelector<const std::type_info*, VALUE> +{ + typedef LLTypeInfoLookup<VALUE> type; +}; + +template <typename KEY, typename VALUE, typename COMPARATOR = LLRegistryDefaultComparator<KEY> > +class LLRegistry +{ +public: + typedef LLRegistry<KEY, VALUE, COMPARATOR> registry_t; + typedef typename boost::add_reference<typename boost::add_const<KEY>::type>::type ref_const_key_t; + typedef typename boost::add_reference<typename boost::add_const<VALUE>::type>::type ref_const_value_t; + typedef typename boost::add_reference<VALUE>::type ref_value_t; + typedef typename boost::add_pointer<typename boost::add_const<VALUE>::type>::type ptr_const_value_t; + typedef typename boost::add_pointer<VALUE>::type ptr_value_t; + + class Registrar + { + friend class LLRegistry<KEY, VALUE, COMPARATOR>; + public: + typedef typename LLRegistryMapSelector<KEY, VALUE>::type registry_map_t; + + bool add(ref_const_key_t key, ref_const_value_t value) + { + if (mMap.insert(std::make_pair(key, value)).second == false) + { + llwarns << "Tried to register " << key << " but it was already registered!" << llendl; + return false; + } + return true; + } + + void remove(ref_const_key_t key) + { + mMap.erase(key); + } + + void replace(ref_const_key_t key, ref_const_value_t value) + { + mMap[key] = value; + } + + typename registry_map_t::const_iterator beginItems() const + { + return mMap.begin(); + } + + typename registry_map_t::const_iterator endItems() const + { + return mMap.end(); + } + + protected: + ptr_value_t getValue(ref_const_key_t key) + { + typename registry_map_t::iterator found_it = mMap.find(key); + if (found_it != mMap.end()) + { + return &(found_it->second); + } + return NULL; + } + + ptr_const_value_t getValue(ref_const_key_t key) const + { + typename registry_map_t::const_iterator found_it = mMap.find(key); + if (found_it != mMap.end()) + { + return &(found_it->second); + } + return NULL; + } + + // if the registry is used to store pointers, and null values are valid entries + // then use this function to check the existence of an entry + bool exists(ref_const_key_t key) const + { + return mMap.find(key) != mMap.end(); + } + + bool empty() const + { + return mMap.empty(); + } + + protected: + // use currentRegistrar() or defaultRegistrar() + Registrar() {} + ~Registrar() {} + + private: + registry_map_t mMap; + }; + + typedef typename std::list<Registrar*> scope_list_t; + typedef typename std::list<Registrar*>::iterator scope_list_iterator_t; + typedef typename std::list<Registrar*>::const_iterator scope_list_const_iterator_t; + + LLRegistry() + {} + + ~LLRegistry() {} + + ptr_value_t getValue(ref_const_key_t key) + { + for(scope_list_iterator_t it = mActiveScopes.begin(); + it != mActiveScopes.end(); + ++it) + { + ptr_value_t valuep = (*it)->getValue(key); + if (valuep != NULL) return valuep; + } + return mDefaultRegistrar.getValue(key); + } + + ptr_const_value_t getValue(ref_const_key_t key) const + { + for(scope_list_const_iterator_t it = mActiveScopes.begin(); + it != mActiveScopes.end(); + ++it) + { + ptr_value_t valuep = (*it)->getValue(key); + if (valuep != NULL) return valuep; + } + return mDefaultRegistrar.getValue(key); + } + + bool exists(ref_const_key_t key) const + { + for(scope_list_const_iterator_t it = mActiveScopes.begin(); + it != mActiveScopes.end(); + ++it) + { + if ((*it)->exists(key)) return true; + } + + return mDefaultRegistrar.exists(key); + } + + bool empty() const + { + for(scope_list_const_iterator_t it = mActiveScopes.begin(); + it != mActiveScopes.end(); + ++it) + { + if (!(*it)->empty()) return false; + } + + return mDefaultRegistrar.empty(); + } + + + Registrar& defaultRegistrar() + { + return mDefaultRegistrar; + } + + const Registrar& defaultRegistrar() const + { + return mDefaultRegistrar; + } + + + Registrar& currentRegistrar() + { + if (!mActiveScopes.empty()) + { + return *mActiveScopes.front(); + } + + return mDefaultRegistrar; + } + + const Registrar& currentRegistrar() const + { + if (!mActiveScopes.empty()) + { + return *mActiveScopes.front(); + } + + return mDefaultRegistrar; + } + + +protected: + void addScope(Registrar* scope) + { + // newer scopes go up front + mActiveScopes.insert(mActiveScopes.begin(), scope); + } + + void removeScope(Registrar* scope) + { + // O(N) but should be near the beggining and N should be small and this is safer than storing iterators + scope_list_iterator_t iter = std::find(mActiveScopes.begin(), mActiveScopes.end(), scope); + if (iter != mActiveScopes.end()) + { + mActiveScopes.erase(iter); + } + } + +private: + scope_list_t mActiveScopes; + Registrar mDefaultRegistrar; +}; + +template <typename KEY, typename VALUE, typename DERIVED_TYPE, typename COMPARATOR = LLRegistryDefaultComparator<KEY> > +class LLRegistrySingleton + : public LLRegistry<KEY, VALUE, COMPARATOR>, + public LLSingleton<DERIVED_TYPE> +{ + friend class LLSingleton<DERIVED_TYPE>; +public: + typedef LLRegistry<KEY, VALUE, COMPARATOR> registry_t; + typedef const KEY& ref_const_key_t; + typedef const VALUE& ref_const_value_t; + typedef VALUE* ptr_value_t; + typedef const VALUE* ptr_const_value_t; + typedef LLSingleton<DERIVED_TYPE> singleton_t; + + class ScopedRegistrar : public registry_t::Registrar + { + public: + ScopedRegistrar(bool push_scope = true) + { + if (push_scope) + { + pushScope(); + } + } + + ~ScopedRegistrar() + { + if (!singleton_t::destroyed()) + { + popScope(); + } + } + + void pushScope() + { + singleton_t::instance().addScope(this); + } + + void popScope() + { + singleton_t::instance().removeScope(this); + } + + ptr_value_t getValueFromScope(ref_const_key_t key) + { + return getValue(key); + } + + ptr_const_value_t getValueFromScope(ref_const_key_t key) const + { + return getValue(key); + } + + private: + typename std::list<typename registry_t::Registrar*>::iterator mListIt; + }; + + class StaticRegistrar : public registry_t::Registrar + { + public: + virtual ~StaticRegistrar() {} + StaticRegistrar(ref_const_key_t key, ref_const_value_t value) + { + singleton_t::instance().mStaticScope->add(key, value); + } + }; + + // convenience functions + typedef typename LLRegistry<KEY, VALUE, COMPARATOR>::Registrar& ref_registrar_t; + static ref_registrar_t currentRegistrar() + { + return singleton_t::instance().registry_t::currentRegistrar(); + } + + static ref_registrar_t defaultRegistrar() + { + return singleton_t::instance().registry_t::defaultRegistrar(); + } + + static ptr_value_t getValue(ref_const_key_t key) + { + return singleton_t::instance().registry_t::getValue(key); + } + +protected: + // DERIVED_TYPE needs to derive from LLRegistrySingleton + LLRegistrySingleton() + : mStaticScope(NULL) + {} + + virtual void initSingleton() + { + mStaticScope = new ScopedRegistrar(); + } + + virtual ~LLRegistrySingleton() + { + delete mStaticScope; + } + +private: + ScopedRegistrar* mStaticScope; +}; + +// helper macro for doing static registration +#define GLUED_TOKEN(x, y) x ## y +#define GLUE_TOKENS(x, y) GLUED_TOKEN(x, y) +#define LLREGISTER_STATIC(REGISTRY, KEY, VALUE) static REGISTRY::StaticRegistrar GLUE_TOKENS(reg, __LINE__)(KEY, VALUE); + +#endif diff --git a/indra/llcommon/llsdparam.cpp b/indra/llcommon/llsdparam.cpp new file mode 100644 index 0000000000..0e29873bb0 --- /dev/null +++ b/indra/llcommon/llsdparam.cpp @@ -0,0 +1,342 @@ +/** + * @file llsdparam.cpp + * @brief parameter block abstraction for creating complex objects and + * parsing construction parameters from xml and LLSD + * + * $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" + +// Project includes +#include "llsdparam.h" +#include "llsdutil.h" + +static LLInitParam::Parser::parser_read_func_map_t sReadFuncs; +static LLInitParam::Parser::parser_write_func_map_t sWriteFuncs; +static LLInitParam::Parser::parser_inspect_func_map_t sInspectFuncs; +static const LLSD NO_VALUE_MARKER; + +LLFastTimer::DeclareTimer FTM_SD_PARAM_ADAPTOR("LLSD to LLInitParam conversion"); + +// +// LLParamSDParser +// +LLParamSDParser::LLParamSDParser() +: Parser(sReadFuncs, sWriteFuncs, sInspectFuncs) +{ + using boost::bind; + + if (sReadFuncs.empty()) + { + registerParserFuncs<LLInitParam::Flag>(readFlag, &LLParamSDParser::writeFlag); + registerParserFuncs<S32>(readS32, &LLParamSDParser::writeTypedValue<S32>); + registerParserFuncs<U32>(readU32, &LLParamSDParser::writeU32Param); + registerParserFuncs<F32>(readF32, &LLParamSDParser::writeTypedValue<F32>); + registerParserFuncs<F64>(readF64, &LLParamSDParser::writeTypedValue<F64>); + registerParserFuncs<bool>(readBool, &LLParamSDParser::writeTypedValue<bool>); + registerParserFuncs<std::string>(readString, &LLParamSDParser::writeTypedValue<std::string>); + registerParserFuncs<LLUUID>(readUUID, &LLParamSDParser::writeTypedValue<LLUUID>); + registerParserFuncs<LLDate>(readDate, &LLParamSDParser::writeTypedValue<LLDate>); + registerParserFuncs<LLURI>(readURI, &LLParamSDParser::writeTypedValue<LLURI>); + registerParserFuncs<LLSD>(readSD, &LLParamSDParser::writeTypedValue<LLSD>); + } +} + +// special case handling of U32 due to ambiguous LLSD::assign overload +bool LLParamSDParser::writeU32Param(LLParamSDParser::parser_t& parser, const void* val_ptr, parser_t::name_stack_t& name_stack) +{ + LLParamSDParser& sdparser = static_cast<LLParamSDParser&>(parser); + if (!sdparser.mWriteRootSD) return false; + + parser_t::name_stack_range_t range(name_stack.begin(), name_stack.end()); + LLSD& sd_to_write = LLParamSDParserUtilities::getSDWriteNode(*sdparser.mWriteRootSD, range); + sd_to_write.assign((S32)*((const U32*)val_ptr)); + + return true; +} + +bool LLParamSDParser::writeFlag(LLParamSDParser::parser_t& parser, const void* val_ptr, parser_t::name_stack_t& name_stack) +{ + LLParamSDParser& sdparser = static_cast<LLParamSDParser&>(parser); + if (!sdparser.mWriteRootSD) return false; + + parser_t::name_stack_range_t range(name_stack.begin(), name_stack.end()); + LLParamSDParserUtilities::getSDWriteNode(*sdparser.mWriteRootSD, range); + + return true; +} + +void LLParamSDParser::submit(LLInitParam::BaseBlock& block, const LLSD& sd, LLInitParam::Parser::name_stack_t& name_stack) +{ + mCurReadSD = &sd; + block.submitValue(name_stack, *this); +} + +void LLParamSDParser::readSD(const LLSD& sd, LLInitParam::BaseBlock& block, bool silent) +{ + mCurReadSD = NULL; + mNameStack.clear(); + setParseSilently(silent); + + LLParamSDParserUtilities::readSDValues(boost::bind(&LLParamSDParser::submit, this, boost::ref(block), _1, _2), sd, mNameStack); + //readSDValues(sd, block); +} + +void LLParamSDParser::writeSD(LLSD& sd, const LLInitParam::BaseBlock& block) +{ + mNameStack.clear(); + mWriteRootSD = &sd; + + name_stack_t name_stack; + block.serializeBlock(*this, name_stack); +} + +/*virtual*/ std::string LLParamSDParser::getCurrentElementName() +{ + std::string full_name = "sd"; + for (name_stack_t::iterator it = mNameStack.begin(); + it != mNameStack.end(); + ++it) + { + full_name += llformat("[%s]", it->first.c_str()); + } + + return full_name; +} + + +bool LLParamSDParser::readFlag(Parser& parser, void* val_ptr) +{ + LLParamSDParser& self = static_cast<LLParamSDParser&>(parser); + return self.mCurReadSD == &NO_VALUE_MARKER; +} + + +bool LLParamSDParser::readS32(Parser& parser, void* val_ptr) +{ + LLParamSDParser& self = static_cast<LLParamSDParser&>(parser); + + *((S32*)val_ptr) = self.mCurReadSD->asInteger(); + return true; +} + +bool LLParamSDParser::readU32(Parser& parser, void* val_ptr) +{ + LLParamSDParser& self = static_cast<LLParamSDParser&>(parser); + + *((U32*)val_ptr) = self.mCurReadSD->asInteger(); + return true; +} + +bool LLParamSDParser::readF32(Parser& parser, void* val_ptr) +{ + LLParamSDParser& self = static_cast<LLParamSDParser&>(parser); + + *((F32*)val_ptr) = self.mCurReadSD->asReal(); + return true; +} + +bool LLParamSDParser::readF64(Parser& parser, void* val_ptr) +{ + LLParamSDParser& self = static_cast<LLParamSDParser&>(parser); + + *((F64*)val_ptr) = self.mCurReadSD->asReal(); + return true; +} + +bool LLParamSDParser::readBool(Parser& parser, void* val_ptr) +{ + LLParamSDParser& self = static_cast<LLParamSDParser&>(parser); + + *((bool*)val_ptr) = self.mCurReadSD->asBoolean(); + return true; +} + +bool LLParamSDParser::readString(Parser& parser, void* val_ptr) +{ + LLParamSDParser& self = static_cast<LLParamSDParser&>(parser); + + *((std::string*)val_ptr) = self.mCurReadSD->asString(); + return true; +} + +bool LLParamSDParser::readUUID(Parser& parser, void* val_ptr) +{ + LLParamSDParser& self = static_cast<LLParamSDParser&>(parser); + + *((LLUUID*)val_ptr) = self.mCurReadSD->asUUID(); + return true; +} + +bool LLParamSDParser::readDate(Parser& parser, void* val_ptr) +{ + LLParamSDParser& self = static_cast<LLParamSDParser&>(parser); + + *((LLDate*)val_ptr) = self.mCurReadSD->asDate(); + return true; +} + +bool LLParamSDParser::readURI(Parser& parser, void* val_ptr) +{ + LLParamSDParser& self = static_cast<LLParamSDParser&>(parser); + + *((LLURI*)val_ptr) = self.mCurReadSD->asURI(); + return true; +} + +bool LLParamSDParser::readSD(Parser& parser, void* val_ptr) +{ + LLParamSDParser& self = static_cast<LLParamSDParser&>(parser); + + *((LLSD*)val_ptr) = *self.mCurReadSD; + return true; +} + +// static +LLSD& LLParamSDParserUtilities::getSDWriteNode(LLSD& input, LLInitParam::Parser::name_stack_range_t& name_stack_range) +{ + LLSD* sd_to_write = &input; + + for (LLInitParam::Parser::name_stack_t::iterator it = name_stack_range.first; + it != name_stack_range.second; + ++it) + { + bool new_traversal = it->second; + + LLSD* child_sd = it->first.empty() ? sd_to_write : &(*sd_to_write)[it->first]; + + if (child_sd->isArray()) + { + if (new_traversal) + { + // write to new element at end + sd_to_write = &(*child_sd)[child_sd->size()]; + } + else + { + // write to last of existing elements, or first element if empty + sd_to_write = &(*child_sd)[llmax(0, child_sd->size() - 1)]; + } + } + else + { + if (new_traversal + && child_sd->isDefined() + && !child_sd->isArray()) + { + // copy child contents into first element of an array + LLSD new_array = LLSD::emptyArray(); + new_array.append(*child_sd); + // assign array to slot that previously held the single value + *child_sd = new_array; + // return next element in that array + sd_to_write = &((*child_sd)[1]); + } + else + { + sd_to_write = child_sd; + } + } + it->second = false; + } + + return *sd_to_write; +} + +//static +void LLParamSDParserUtilities::readSDValues(read_sd_cb_t cb, const LLSD& sd, LLInitParam::Parser::name_stack_t& stack) +{ + if (sd.isMap()) + { + for (LLSD::map_const_iterator it = sd.beginMap(); + it != sd.endMap(); + ++it) + { + stack.push_back(make_pair(it->first, true)); + readSDValues(cb, it->second, stack); + stack.pop_back(); + } + } + else if (sd.isArray()) + { + for (LLSD::array_const_iterator it = sd.beginArray(); + it != sd.endArray(); + ++it) + { + stack.back().second = true; + readSDValues(cb, *it, stack); + } + } + else if (sd.isUndefined()) + { + if (!cb.empty()) + { + cb(NO_VALUE_MARKER, stack); + } + } + else + { + if (!cb.empty()) + { + cb(sd, stack); + } + } +} + +//static +void LLParamSDParserUtilities::readSDValues(read_sd_cb_t cb, const LLSD& sd) +{ + LLInitParam::Parser::name_stack_t stack = LLInitParam::Parser::name_stack_t(); + readSDValues(cb, sd, stack); +} +namespace LLInitParam +{ + // LLSD specialization + // block param interface + bool ParamValue<LLSD, TypeValues<LLSD>, false>::deserializeBlock(Parser& p, Parser::name_stack_range_t name_stack, bool new_name) + { + LLSD& sd = LLParamSDParserUtilities::getSDWriteNode(mValue, name_stack); + + LLSD::String string; + + if (p.readValue<LLSD::String>(string)) + { + sd = string; + return true; + } + return false; + } + + //static + void ParamValue<LLSD, TypeValues<LLSD>, false>::serializeElement(Parser& p, const LLSD& sd, Parser::name_stack_t& name_stack) + { + p.writeValue<LLSD::String>(sd.asString(), name_stack); + } + + void ParamValue<LLSD, TypeValues<LLSD>, false>::serializeBlock(Parser& p, Parser::name_stack_t& name_stack, const BaseBlock* diff_block) const + { + // read from LLSD value and serialize out to parser (which could be LLSD, XUI, etc) + Parser::name_stack_t stack; + LLParamSDParserUtilities::readSDValues(boost::bind(&serializeElement, boost::ref(p), _1, _2), mValue, stack); + } +} diff --git a/indra/llcommon/llsdparam.h b/indra/llcommon/llsdparam.h new file mode 100644 index 0000000000..6ef5debd7b --- /dev/null +++ b/indra/llcommon/llsdparam.h @@ -0,0 +1,126 @@ +/** + * @file llsdparam.h + * @brief parameter block abstraction for creating complex objects and + * parsing construction parameters from xml and LLSD + * + * $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_LLSDPARAM_H +#define LL_LLSDPARAM_H + +#include "llinitparam.h" +#include "boost/function.hpp" + +struct LL_COMMON_API LLParamSDParserUtilities +{ + static LLSD& getSDWriteNode(LLSD& input, LLInitParam::Parser::name_stack_range_t& name_stack_range); + + typedef boost::function<void (const LLSD&, LLInitParam::Parser::name_stack_t&)> read_sd_cb_t; + static void readSDValues(read_sd_cb_t cb, const LLSD& sd, LLInitParam::Parser::name_stack_t& stack); + static void readSDValues(read_sd_cb_t cb, const LLSD& sd); +}; + +class LL_COMMON_API LLParamSDParser +: public LLInitParam::Parser +{ +LOG_CLASS(LLParamSDParser); + +typedef LLInitParam::Parser parser_t; + +public: + LLParamSDParser(); + void readSD(const LLSD& sd, LLInitParam::BaseBlock& block, bool silent = false); + void writeSD(LLSD& sd, const LLInitParam::BaseBlock& block); + + /*virtual*/ std::string getCurrentElementName(); + +private: + void submit(LLInitParam::BaseBlock& block, const LLSD& sd, LLInitParam::Parser::name_stack_t& name_stack); + + template<typename T> + static bool writeTypedValue(Parser& parser, const void* val_ptr, parser_t::name_stack_t& name_stack) + { + LLParamSDParser& sdparser = static_cast<LLParamSDParser&>(parser); + if (!sdparser.mWriteRootSD) return false; + + LLInitParam::Parser::name_stack_range_t range(name_stack.begin(), name_stack.end()); + LLSD& sd_to_write = LLParamSDParserUtilities::getSDWriteNode(*sdparser.mWriteRootSD, range); + + sd_to_write.assign(*((const T*)val_ptr)); + return true; + } + + static bool writeU32Param(Parser& parser, const void* value_ptr, parser_t::name_stack_t& name_stack); + static bool writeFlag(Parser& parser, const void* value_ptr, parser_t::name_stack_t& name_stack); + + static bool readFlag(Parser& parser, void* val_ptr); + static bool readS32(Parser& parser, void* val_ptr); + static bool readU32(Parser& parser, void* val_ptr); + static bool readF32(Parser& parser, void* val_ptr); + static bool readF64(Parser& parser, void* val_ptr); + static bool readBool(Parser& parser, void* val_ptr); + static bool readString(Parser& parser, void* val_ptr); + static bool readUUID(Parser& parser, void* val_ptr); + static bool readDate(Parser& parser, void* val_ptr); + static bool readURI(Parser& parser, void* val_ptr); + static bool readSD(Parser& parser, void* val_ptr); + + Parser::name_stack_t mNameStack; + const LLSD* mCurReadSD; + LLSD* mWriteRootSD; + LLSD* mCurWriteSD; +}; + + +extern LL_COMMON_API LLFastTimer::DeclareTimer FTM_SD_PARAM_ADAPTOR; +template<typename T> +class LLSDParamAdapter : public T +{ +public: + LLSDParamAdapter() {} + LLSDParamAdapter(const LLSD& sd) + { + LLFastTimer _(FTM_SD_PARAM_ADAPTOR); + LLParamSDParser parser; + // don't spam for implicit parsing of LLSD, as we want to allow arbitrary freeform data and ignore most of it + bool parse_silently = true; + parser.readSD(sd, *this, parse_silently); + } + + operator LLSD() const + { + LLParamSDParser parser; + LLSD sd; + parser.writeSD(sd, *this); + return sd; + } + + LLSDParamAdapter(const T& val) + : T(val) + { + T::operator=(val); + } +}; + +#endif // LL_LLSDPARAM_H + diff --git a/indra/llcommon/llsortedvector.h b/indra/llcommon/llsortedvector.h new file mode 100644 index 0000000000..391b82ee44 --- /dev/null +++ b/indra/llcommon/llsortedvector.h @@ -0,0 +1,152 @@ +/** + * @file llsortedvector.h + * @author Nat Goodspeed + * @date 2012-04-08 + * @brief LLSortedVector class wraps a vector that we maintain in sorted + * order so we can perform binary-search lookups. + * + * $LicenseInfo:firstyear=2012&license=viewerlgpl$ + * Copyright (c) 2012, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_LLSORTEDVECTOR_H) +#define LL_LLSORTEDVECTOR_H + +#include <vector> +#include <algorithm> + +/** + * LLSortedVector contains a std::vector<std::pair> that we keep sorted on the + * first of the pair. This makes insertion somewhat more expensive than simple + * std::vector::push_back(), but allows us to use binary search for lookups. + * It's intended for small aggregates where lookup is far more performance- + * critical than insertion; in such cases a binary search on a small, sorted + * std::vector can be more performant than a std::map lookup. + */ +template <typename KEY, typename VALUE> +class LLSortedVector +{ +public: + typedef LLSortedVector<KEY, VALUE> self; + typedef KEY key_type; + typedef VALUE mapped_type; + typedef std::pair<key_type, mapped_type> value_type; + typedef std::vector<value_type> PairVector; + typedef typename PairVector::iterator iterator; + typedef typename PairVector::const_iterator const_iterator; + + /// Empty + LLSortedVector() {} + + /// Fixed initial size + LLSortedVector(std::size_t size): + mVector(size) + {} + + /// Bulk load + template <typename ITER> + LLSortedVector(ITER begin, ITER end): + mVector(begin, end) + { + // Allow caller to dump in a bunch of (pairs convertible to) + // value_type if desired, but make sure we sort afterwards. + std::sort(mVector.begin(), mVector.end()); + } + + /// insert(key, value) + std::pair<iterator, bool> insert(const key_type& key, const mapped_type& value) + { + return insert(value_type(key, value)); + } + + /// insert(value_type) + std::pair<iterator, bool> insert(const value_type& pair) + { + typedef std::pair<iterator, bool> iterbool; + iterator found = std::lower_bound(mVector.begin(), mVector.end(), pair, + less<value_type>()); + // have to check for end() before it's even valid to dereference + if (found == mVector.end()) + { + std::size_t index(mVector.size()); + mVector.push_back(pair); + // don't forget that push_back() invalidates 'found' + return iterbool(mVector.begin() + index, true); + } + if (found->first == pair.first) + { + return iterbool(found, false); + } + // remember that insert() invalidates 'found' -- save index + std::size_t index(found - mVector.begin()); + mVector.insert(found, pair); + // okay, convert from index back to iterator + return iterbool(mVector.begin() + index, true); + } + + iterator begin() { return mVector.begin(); } + iterator end() { return mVector.end(); } + const_iterator begin() const { return mVector.begin(); } + const_iterator end() const { return mVector.end(); } + + bool empty() const { return mVector.empty(); } + std::size_t size() const { return mVector.size(); } + + /// find + iterator find(const key_type& key) + { + iterator found = std::lower_bound(mVector.begin(), mVector.end(), + value_type(key, mapped_type()), + less<value_type>()); + if (found == mVector.end() || found->first != key) + return mVector.end(); + return found; + } + + const_iterator find(const key_type& key) const + { + return const_cast<self*>(this)->find(key); + } + +private: + // Define our own 'less' comparator so we can specialize without messing + // with std::less. + template <typename T> + struct less: public std::less<T> {}; + + // Specialize 'less' for an LLSortedVector::value_type involving + // std::type_info*. This is one of LLSortedVector's foremost use cases. We + // specialize 'less' rather than just defining a specific comparator + // because LLSortedVector should be usable for other key_types as well. + template <typename T> + struct less< std::pair<std::type_info*, T> >: + public std::binary_function<std::pair<std::type_info*, T>, + std::pair<std::type_info*, T>, + bool> + { + bool operator()(const std::pair<std::type_info*, T>& lhs, + const std::pair<std::type_info*, T>& rhs) const + { + return lhs.first->before(*rhs.first); + } + }; + + // Same as above, but with const std::type_info*. + template <typename T> + struct less< std::pair<const std::type_info*, T> >: + public std::binary_function<std::pair<const std::type_info*, T>, + std::pair<const std::type_info*, T>, + bool> + { + bool operator()(const std::pair<const std::type_info*, T>& lhs, + const std::pair<const std::type_info*, T>& rhs) const + { + return lhs.first->before(*rhs.first); + } + }; + + PairVector mVector; +}; + +#endif /* ! defined(LL_LLSORTEDVECTOR_H) */ 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 3a27badb5a..119efc7957 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: @@ -240,40 +241,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 @@ -281,8 +319,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 @@ -290,19 +328,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 @@ -311,18 +362,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 @@ -330,24 +381,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 @@ -355,7 +406,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); } @@ -365,7 +416,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; @@ -639,10 +690,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> @@ -672,7 +1038,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()); } @@ -698,8 +1064,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()); @@ -709,10 +1075,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()); @@ -723,7 +1089,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(); @@ -764,7 +1130,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(); @@ -799,7 +1165,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() ) { @@ -813,7 +1179,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() ) { @@ -827,7 +1193,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() ) { @@ -841,7 +1207,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() ) { @@ -856,7 +1222,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() ) { @@ -875,7 +1241,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; @@ -917,7 +1283,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; @@ -938,10 +1304,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 @@ -950,10 +1316,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 @@ -962,7 +1328,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; @@ -982,12 +1348,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++) { @@ -1006,7 +1372,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; @@ -1023,7 +1389,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; @@ -1054,8 +1420,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++) @@ -1085,7 +1486,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() ) { @@ -1095,7 +1496,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; @@ -1106,7 +1507,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() ) { @@ -1122,8 +1523,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; @@ -1133,8 +1534,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); @@ -1144,14 +1545,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") || @@ -1181,7 +1582,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); @@ -1194,7 +1595,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); @@ -1207,7 +1608,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); @@ -1220,7 +1621,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); @@ -1233,17 +1634,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; @@ -1253,17 +1654,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 @@ -1280,7 +1681,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); @@ -1293,17 +1694,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 @@ -1320,7 +1721,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/lltypeinfolookup.h b/indra/llcommon/lltypeinfolookup.h new file mode 100644 index 0000000000..7510cc12ed --- /dev/null +++ b/indra/llcommon/lltypeinfolookup.h @@ -0,0 +1,110 @@ +/** + * @file lltypeinfolookup.h + * @author Nat Goodspeed + * @date 2012-04-08 + * @brief Template data structure like std::map<std::type_info*, T> + * + * $LicenseInfo:firstyear=2012&license=viewerlgpl$ + * Copyright (c) 2012, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_LLTYPEINFOLOOKUP_H) +#define LL_LLTYPEINFOLOOKUP_H + +#include "llsortedvector.h" +#include <typeinfo> + +/** + * LLTypeInfoLookup is specifically designed for use cases for which you might + * consider std::map<std::type_info*, VALUE>. We have several such data + * structures in the viewer. The trouble with them is that at least on Linux, + * you can't rely on always getting the same std::type_info* for a given type: + * different load modules will produce different std::type_info*. + * LLTypeInfoLookup contains a workaround to address this issue. + * + * Specifically, when we don't find the passed std::type_info*, + * LLTypeInfoLookup performs a linear search over registered entries to + * compare name() strings. Presuming that this succeeds, we cache the new + * (previously unrecognized) std::type_info* to speed future lookups. + * + * This worst-case fallback search (linear search with string comparison) + * should only happen the first time we look up a given type from a particular + * load module other than the one from which we initially registered types. + * (However, a lookup which wouldn't succeed anyway will always have + * worst-case performance.) This class is probably best used with less than a + * few dozen different types. + */ +template <typename VALUE> +class LLTypeInfoLookup +{ +public: + typedef LLTypeInfoLookup<VALUE> self; + typedef LLSortedVector<const std::type_info*, VALUE> vector_type; + typedef typename vector_type::key_type key_type; + typedef typename vector_type::mapped_type mapped_type; + typedef typename vector_type::value_type value_type; + typedef typename vector_type::iterator iterator; + typedef typename vector_type::const_iterator const_iterator; + + LLTypeInfoLookup() {} + + iterator begin() { return mVector.begin(); } + iterator end() { return mVector.end(); } + const_iterator begin() const { return mVector.begin(); } + const_iterator end() const { return mVector.end(); } + bool empty() const { return mVector.empty(); } + std::size_t size() const { return mVector.size(); } + + std::pair<iterator, bool> insert(const std::type_info* key, const VALUE& value) + { + return insert(value_type(key, value)); + } + + std::pair<iterator, bool> insert(const value_type& pair) + { + return mVector.insert(pair); + } + + // const find() forwards to non-const find(): this can alter mVector! + const_iterator find(const std::type_info* key) const + { + return const_cast<self*>(this)->find(key); + } + + // non-const find() caches previously-unknown type_info* to speed future + // lookups. + iterator find(const std::type_info* key) + { + iterator found = mVector.find(key); + if (found != mVector.end()) + { + // If LLSortedVector::find() found, great, we're done. + return found; + } + // Here we didn't find the passed type_info*. On Linux, though, even + // for the same type, typeid(sametype) produces a different type_info* + // when used in different load modules. So the fact that we didn't + // find the type_info* we seek doesn't mean this type isn't + // registered. Scan for matching name() string. + for (typename vector_type::iterator ti(mVector.begin()), tend(mVector.end()); + ti != tend; ++ti) + { + if (std::string(ti->first->name()) == key->name()) + { + // This unrecognized 'key' is for the same type as ti->first. + // To speed future lookups, insert a new entry that lets us + // look up ti->second using this same 'key'. + return insert(key, ti->second).first; + } + } + // We simply have never seen a type with this type_info* from any load + // module. + return mVector.end(); + } + +private: + vector_type mVector; +}; + +#endif /* ! defined(LL_LLTYPEINFOLOOKUP_H) */ diff --git a/indra/llcommon/llversionviewer.h b/indra/llcommon/llversionviewer.h index fafc750690..22d0aba51b 100644 --- a/indra/llcommon/llversionviewer.h +++ b/indra/llcommon/llversionviewer.h @@ -29,7 +29,7 @@ const S32 LL_VERSION_MAJOR = 3; const S32 LL_VERSION_MINOR = 3; -const S32 LL_VERSION_PATCH = 2; +const S32 LL_VERSION_PATCH = 4; const S32 LL_VERSION_BUILD = 0; const char * const LL_CHANNEL = "Second Life Developer"; 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/llerror_test.cpp b/indra/llcommon/tests/llerror_test.cpp index 09a20231de..279a90e51b 100644 --- a/indra/llcommon/tests/llerror_test.cpp +++ b/indra/llcommon/tests/llerror_test.cpp @@ -1,4 +1,4 @@ -/** +/** * @file llerror_test.cpp * @date December 2006 * @brief error unit tests @@ -6,21 +6,21 @@ * $LicenseInfo:firstyear=2006&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$ */ @@ -49,7 +49,7 @@ namespace static bool fatalWasCalled; void fatalCall(const std::string&) { fatalWasCalled = true; } } - + namespace tut { class TestRecorder : public LLError::Recorder @@ -57,59 +57,65 @@ namespace tut public: TestRecorder() : mWantsTime(false) { } ~TestRecorder() { LLError::removeRecorder(this); } - + void recordMessage(LLError::ELevel level, const std::string& message) { mMessages.push_back(message); } - + int countMessages() { return (int) mMessages.size(); } void clearMessages() { mMessages.clear(); } - + void setWantsTime(bool t) { mWantsTime = t; } bool wantsTime() { return mWantsTime; } - + std::string message(int n) { std::ostringstream test_name; test_name << "testing message " << n << ", not enough messages"; - + tut::ensure(test_name.str(), n < countMessages()); return mMessages[n]; } - + private: typedef std::vector<std::string> MessageVector; MessageVector mMessages; - + bool mWantsTime; }; struct ErrorTestData { - TestRecorder mRecorder; + // addRecorder() expects to be able to later delete the passed + // Recorder*. Even though removeRecorder() reclaims ownership, passing + // a pointer to a data member rather than a heap Recorder subclass + // instance would just be Wrong. + TestRecorder* mRecorder; LLError::Settings* mPriorErrorSettings; - - ErrorTestData() + + ErrorTestData(): + mRecorder(new TestRecorder) { fatalWasCalled = false; - + mPriorErrorSettings = LLError::saveAndResetSettings(); LLError::setDefaultLevel(LLError::LEVEL_DEBUG); LLError::setFatalFunction(fatalCall); - LLError::addRecorder(&mRecorder); + LLError::addRecorder(mRecorder); } - + ~ErrorTestData() { - LLError::removeRecorder(&mRecorder); + LLError::removeRecorder(mRecorder); + delete mRecorder; LLError::restoreSettings(mPriorErrorSettings); } - + void ensure_message_count(int expectedCount) { - ensure_equals("message count", mRecorder.countMessages(), expectedCount); + ensure_equals("message count", mRecorder->countMessages(), expectedCount); } void ensure_message_contains(int n, const std::string& expectedText) @@ -117,7 +123,7 @@ namespace tut std::ostringstream test_name; test_name << "testing message " << n; - ensure_contains(test_name.str(), mRecorder.message(n), expectedText); + ensure_contains(test_name.str(), mRecorder->message(n), expectedText); } void ensure_message_does_not_contain(int n, const std::string& expectedText) @@ -125,22 +131,22 @@ namespace tut std::ostringstream test_name; test_name << "testing message " << n; - ensure_does_not_contain(test_name.str(), mRecorder.message(n), expectedText); + ensure_does_not_contain(test_name.str(), mRecorder->message(n), expectedText); } }; - + typedef test_group<ErrorTestData> ErrorTestGroup; typedef ErrorTestGroup::object ErrorTestObject; - + ErrorTestGroup errorTestGroup("error"); - + template<> template<> void ErrorTestObject::test<1>() // basic test of output { llinfos << "test" << llendl; llinfos << "bob" << llendl; - + ensure_message_contains(0, "test"); ensure_message_contains(1, "bob"); } @@ -159,7 +165,7 @@ namespace }; namespace tut -{ +{ template<> template<> void ErrorTestObject::test<2>() // messages are filtered based on default level @@ -172,7 +178,7 @@ namespace tut ensure_message_contains(3, "error"); ensure_message_contains(4, "four"); ensure_message_count(5); - + LLError::setDefaultLevel(LLError::LEVEL_INFO); writeSome(); ensure_message_contains(5, "two"); @@ -180,20 +186,20 @@ namespace tut ensure_message_contains(7, "error"); ensure_message_contains(8, "four"); ensure_message_count(9); - + LLError::setDefaultLevel(LLError::LEVEL_WARN); writeSome(); ensure_message_contains(9, "three"); ensure_message_contains(10, "error"); ensure_message_contains(11, "four"); ensure_message_count(12); - + LLError::setDefaultLevel(LLError::LEVEL_ERROR); writeSome(); ensure_message_contains(12, "error"); ensure_message_contains(13, "four"); ensure_message_count(14); - + LLError::setDefaultLevel(LLError::LEVEL_NONE); writeSome(); ensure_message_count(14); @@ -218,14 +224,14 @@ namespace tut { std::string thisFile = __FILE__; std::string abbreviateFile = LLError::abbreviateFile(thisFile); - + ensure_ends_with("file name abbreviation", abbreviateFile, "llcommon/tests/llerror_test.cpp" ); ensure_does_not_contain("file name abbreviation", abbreviateFile, "indra"); - + std::string someFile = #if LL_WINDOWS "C:/amy/bob/cam.cpp" @@ -234,12 +240,12 @@ namespace tut #endif ; std::string someAbbreviation = LLError::abbreviateFile(someFile); - + ensure_equals("non-indra file abbreviation", someAbbreviation, someFile); } } - + namespace { std::string locationString(int line) @@ -247,22 +253,22 @@ namespace std::ostringstream location; location << LLError::abbreviateFile(__FILE__) << "(" << line << ") : "; - + return location.str(); } - + std::string writeReturningLocation() { llinfos << "apple" << llendl; int this_line = __LINE__; return locationString(this_line); } - + std::string writeReturningLocationAndFunction() { llinfos << "apple" << llendl; int this_line = __LINE__; return locationString(this_line) + __FUNCTION__; } - + std::string errorReturningLocation() { llerrs << "die" << llendl; int this_line = __LINE__; @@ -271,20 +277,20 @@ namespace } namespace tut -{ +{ template<> template<> void ErrorTestObject::test<5>() // file and line information in log messages { std::string location = writeReturningLocation(); // expecting default to not print location information - + LLError::setPrintLocation(true); writeReturningLocation(); - + LLError::setPrintLocation(false); writeReturningLocation(); - + ensure_message_does_not_contain(0, location); ensure_message_contains(1, location); ensure_message_does_not_contain(2, location); @@ -297,7 +303,7 @@ namespace tut existing log messages often do.) The functions all return their C++ name so that test can be substantial mechanized. */ - + std::string logFromGlobal(bool id) { llinfos << (id ? "logFromGlobal: " : "") << "hi" << llendl; @@ -345,7 +351,7 @@ namespace return "ClassWithNoLogType::logFromStatic"; } }; - + class ClassWithLogType { LOG_CLASS(ClassWithLogType); public: @@ -360,13 +366,13 @@ namespace return "ClassWithLogType::logFromStatic"; } }; - + std::string logFromNamespace(bool id) { return Foo::logFromNamespace(id); } std::string logFromClassWithNoLogTypeMember(bool id) { ClassWithNoLogType c; return c.logFromMember(id); } std::string logFromClassWithNoLogTypeStatic(bool id) { return ClassWithNoLogType::logFromStatic(id); } std::string logFromClassWithLogTypeMember(bool id) { ClassWithLogType c; return c.logFromMember(id); } std::string logFromClassWithLogTypeStatic(bool id) { return ClassWithLogType::logFromStatic(id); } - + void ensure_has(const std::string& message, const std::string& actual, const std::string& expected) { @@ -379,18 +385,18 @@ namespace throw tut::failure(ss.str().c_str()); } } - + typedef std::string (*LogFromFunction)(bool); - void testLogName(tut::TestRecorder& recorder, LogFromFunction f, + void testLogName(tut::TestRecorder* recorder, LogFromFunction f, const std::string& class_name = "") { - recorder.clearMessages(); + recorder->clearMessages(); std::string name = f(false); f(true); - - std::string messageWithoutName = recorder.message(0); - std::string messageWithName = recorder.message(1); - + + std::string messageWithoutName = recorder->message(0); + std::string messageWithName = recorder->message(1); + ensure_has(name + " logged without name", messageWithoutName, name); ensure_has(name + " logged with name", @@ -431,18 +437,18 @@ namespace llinfos << "inside" << llendl; return "moo"; } - + std::string outerLogger() { llinfos << "outside(" << innerLogger() << ")" << llendl; return "bar"; } - + void uberLogger() { llinfos << "uber(" << outerLogger() << "," << innerLogger() << ")" << llendl; } - + class LogWhileLogging { public: @@ -461,11 +467,11 @@ namespace LogWhileLogging l; llinfos << "meta(" << l << ")" << llendl; } - + } namespace tut -{ +{ template<> template<> // handle nested logging void ErrorTestObject::test<7>() @@ -474,31 +480,31 @@ namespace tut ensure_message_contains(0, "inside"); ensure_message_contains(1, "outside(moo)"); ensure_message_count(2); - + uberLogger(); ensure_message_contains(2, "inside"); ensure_message_contains(3, "inside"); ensure_message_contains(4, "outside(moo)"); ensure_message_contains(5, "uber(bar,moo)"); ensure_message_count(6); - + metaLogger(); ensure_message_contains(6, "logging"); ensure_message_contains(7, "meta(baz)"); ensure_message_count(8); } - + template<> template<> // special handling of llerrs calls void ErrorTestObject::test<8>() { LLError::setPrintLocation(false); std::string location = errorReturningLocation(); - + ensure_message_contains(0, location + "error"); ensure_message_contains(1, "die"); ensure_message_count(2); - + ensure("fatal callback called", fatalWasCalled); } } @@ -509,7 +515,7 @@ namespace { return "1947-07-08T03:04:05Z"; } - + void ufoSighting() { llinfos << "ufo" << llendl; @@ -517,35 +523,35 @@ namespace } namespace tut -{ +{ template<> template<> // time in output (for recorders that need it) void ErrorTestObject::test<9>() { LLError::setTimeFunction(roswell); - mRecorder.setWantsTime(false); + mRecorder->setWantsTime(false); ufoSighting(); ensure_message_contains(0, "ufo"); ensure_message_does_not_contain(0, roswell()); - - mRecorder.setWantsTime(true); + + mRecorder->setWantsTime(true); ufoSighting(); ensure_message_contains(1, "ufo"); ensure_message_contains(1, roswell()); } - + template<> template<> // output order void ErrorTestObject::test<10>() { LLError::setPrintLocation(true); LLError::setTimeFunction(roswell); - mRecorder.setWantsTime(true); + mRecorder->setWantsTime(true); std::string locationAndFunction = writeReturningLocationAndFunction(); - + ensure_equals("order is time type location function message", - mRecorder.message(0), + mRecorder->message(0), roswell() + " INFO: " + locationAndFunction + ": apple"); } @@ -553,30 +559,30 @@ namespace tut // multiple recorders void ErrorTestObject::test<11>() { - TestRecorder altRecorder; - LLError::addRecorder(&altRecorder); - + TestRecorder* altRecorder(new TestRecorder); + LLError::addRecorder(altRecorder); + llinfos << "boo" << llendl; ensure_message_contains(0, "boo"); - ensure_equals("alt recorder count", altRecorder.countMessages(), 1); - ensure_contains("alt recorder message 0", altRecorder.message(0), "boo"); - + ensure_equals("alt recorder count", altRecorder->countMessages(), 1); + ensure_contains("alt recorder message 0", altRecorder->message(0), "boo"); + LLError::setTimeFunction(roswell); - TestRecorder anotherRecorder; - anotherRecorder.setWantsTime(true); - LLError::addRecorder(&anotherRecorder); - + TestRecorder* anotherRecorder(new TestRecorder); + anotherRecorder->setWantsTime(true); + LLError::addRecorder(anotherRecorder); + llinfos << "baz" << llendl; std::string when = roswell(); - + ensure_message_does_not_contain(1, when); - ensure_equals("alt recorder count", altRecorder.countMessages(), 2); - ensure_does_not_contain("alt recorder message 1", altRecorder.message(1), when); - ensure_equals("another recorder count", anotherRecorder.countMessages(), 1); - ensure_contains("another recorder message 0", anotherRecorder.message(0), when); + ensure_equals("alt recorder count", altRecorder->countMessages(), 2); + ensure_does_not_contain("alt recorder message 1", altRecorder->message(1), when); + ensure_equals("another recorder count", anotherRecorder->countMessages(), 1); + ensure_contains("another recorder message 0", anotherRecorder->message(0), when); } } @@ -610,10 +616,10 @@ namespace tut { LLError::setDefaultLevel(LLError::LEVEL_WARN); LLError::setClassLevel("TestBeta", LLError::LEVEL_INFO); - + TestAlpha::doAll(); TestBeta::doAll(); - + ensure_message_contains(0, "aim west"); ensure_message_contains(1, "error"); ensure_message_contains(2, "ate eels"); @@ -623,7 +629,7 @@ namespace tut ensure_message_contains(6, "big easy"); ensure_message_count(7); } - + template<> template<> // filtering by function, and that it will override class filtering void ErrorTestObject::test<13>() @@ -632,13 +638,13 @@ namespace tut LLError::setClassLevel("TestBeta", LLError::LEVEL_WARN); LLError::setFunctionLevel("TestBeta::doInfo", LLError::LEVEL_DEBUG); LLError::setFunctionLevel("TestBeta::doError", LLError::LEVEL_NONE); - + TestBeta::doAll(); ensure_message_contains(0, "buy iron"); ensure_message_contains(1, "bad word"); ensure_message_count(2); } - + template<> template<> // filtering by file // and that it is overridden by both class and function filtering @@ -652,7 +658,7 @@ namespace tut LLError::LEVEL_NONE); LLError::setFunctionLevel("TestBeta::doError", LLError::LEVEL_NONE); - + TestAlpha::doAll(); TestBeta::doAll(); ensure_message_contains(0, "any idea"); @@ -660,7 +666,7 @@ namespace tut ensure_message_contains(2, "bad word"); ensure_message_count(3); } - + template<> template<> // proper cached, efficient lookup of filtering void ErrorTestObject::test<15>() @@ -690,7 +696,7 @@ namespace tut ensure_message_count(2); ensure_equals("sixth check", LLError::shouldLogCallCount(), 3); } - + template<> template<> // configuration from LLSD void ErrorTestObject::test<16>() @@ -699,26 +705,26 @@ namespace tut LLSD config; config["print-location"] = true; config["default-level"] = "DEBUG"; - + LLSD set1; set1["level"] = "WARN"; set1["files"][0] = this_file; - + LLSD set2; set2["level"] = "INFO"; set2["classes"][0] = "TestAlpha"; - + LLSD set3; set3["level"] = "NONE"; set3["functions"][0] = "TestAlpha::doError"; set3["functions"][1] = "TestBeta::doError"; - + config["settings"][0] = set1; config["settings"][1] = set2; config["settings"][2] = set3; - + LLError::configure(config); - + TestAlpha::doAll(); TestBeta::doAll(); ensure_message_contains(0, "any idea"); @@ -726,13 +732,13 @@ namespace tut ensure_message_contains(1, "aim west"); ensure_message_contains(2, "bad word"); ensure_message_count(3); - + // make sure reconfiguring works LLSD config2; config2["default-level"] = "WARN"; - + LLError::configure(config2); - + TestAlpha::doAll(); TestBeta::doAll(); ensure_message_contains(3, "aim west"); @@ -744,13 +750,13 @@ namespace tut ensure_message_contains(8, "big easy"); ensure_message_count(9); } -} +} /* Tests left: handling of classes without LOG_CLASS - live update of filtering from file - + live update of filtering from file + syslog recorder file recorder cerr/stderr recorder 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..a4d3a4e026 100644 --- a/indra/llcommon/tests/wrapllerrs.h +++ b/indra/llcommon/tests/wrapllerrs.h @@ -29,7 +29,22 @@ #if ! defined(LL_WRAPLLERRS_H) #define LL_WRAPLLERRS_H +#if LL_WINDOWS +#pragma warning (disable : 4355) // 'this' used in initializer list: yes, intentionally +#endif + +#include <tut/tut.hpp> #include "llerrorcontrol.h" +#include "stringize.h" +#include <boost/bind.hpp> +#include <boost/noncopyable.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 +85,118 @@ struct WrapLL_ERRS LLError::FatalFunction mPriorFatal; }; +/** + * LLError::addRecorder() accepts ownership of the passed Recorder* -- it + * expects to be able to delete it later. CaptureLog isa Recorder whose + * pointer we want to be able to pass without any ownership implications. + * For such cases, instantiate a new RecorderProxy(yourRecorder) and pass + * that. Your heap RecorderProxy might later be deleted, but not yourRecorder. + */ +class RecorderProxy: public LLError::Recorder +{ +public: + RecorderProxy(LLError::Recorder* recorder): + mRecorder(recorder) + {} + + virtual void recordMessage(LLError::ELevel level, const std::string& message) + { + mRecorder->recordMessage(level, message); + } + + virtual bool wantsTime() + { + return mRecorder->wantsTime(); + } + +private: + LLError::Recorder* mRecorder; +}; + +/** + * Capture log messages. This is adapted (simplified) from the one in + * llerror_test.cpp. + */ +class CaptureLog : public LLError::Recorder, public boost::noncopyable +{ +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()), + mProxy(new RecorderProxy(this)) + { + LLError::setFatalFunction(wouldHaveCrashed); + LLError::setDefaultLevel(level); + LLError::addRecorder(mProxy); + } + + ~CaptureLog() + { + LLError::removeRecorder(mProxy); + delete mProxy; + 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" + << boost::ref(*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; + LLError::Recorder* mProxy; +}; + +inline +std::ostream& operator<<(std::ostream& out, const CaptureLog& log) +{ + return log.streamto(out); +} + #endif /* ! defined(LL_WRAPLLERRS_H) */ |