diff options
author | Brad Kittenbrink <brad@lindenlab.com> | 2009-06-04 16:24:21 +0000 |
---|---|---|
committer | Brad Kittenbrink <brad@lindenlab.com> | 2009-06-04 16:24:21 +0000 |
commit | 087bd265534b8e3086ae1af441a9cf0eb7c684df (patch) | |
tree | a0c911515476d4ac4aac9bd984de28eb7573a478 /indra | |
parent | f9b9372027a41900ad572afcd7ea0d2cc5489b8f (diff) |
Merge of QAR-1383 event-system-7 into trunk.
svn merge -r 121797:121853 svn+ssh://svn.lindenlab.com/svn/linden/branches/merge-event-system-7
Diffstat (limited to 'indra')
51 files changed, 5063 insertions, 245 deletions
diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index d6a9e10707..51bd4354df 100644 --- a/indra/llcommon/CMakeLists.txt +++ b/indra/llcommon/CMakeLists.txt @@ -3,6 +3,7 @@ project(llcommon) include(00-Common) +include(LLAddBuildTest) include(LLCommon) include_directories( @@ -22,9 +23,11 @@ set(llcommon_SOURCE_FILES llcriticaldamp.cpp llcursortypes.cpp lldate.cpp + lldependencies.cpp llerror.cpp llerrorthread.cpp llevent.cpp + llevents.cpp llfasttimer.cpp llfile.cpp llfindlocale.cpp @@ -95,6 +98,7 @@ set(llcommon_HEADER_FILES lldarrayptr.h lldate.h lldefs.h + lldependencies.h lldepthstack.h lldlinked.h lldqueueptr.h @@ -105,6 +109,7 @@ set(llcommon_HEADER_FILES llerrorlegacy.h llerrorthread.h llevent.h + llevents.h lleventemitter.h llextendedstatus.h llfasttimer.h @@ -118,6 +123,7 @@ set(llcommon_HEADER_FILES llhttpstatuscodes.h llindexedqueue.h llkeythrottle.h + lllazy.h lllinkedqueue.h llliveappconfig.h lllivefile.h @@ -176,6 +182,7 @@ set(llcommon_HEADER_FILES stdenums.h stdtypes.h string_table.h + stringize.h timer.h timing.h u64.h @@ -194,3 +201,5 @@ target_link_libraries( ${EXPAT_LIBRARIES} ${ZLIB_LIBRARIES} ) + +ADD_BUILD_TEST(lllazy llcommon) diff --git a/indra/llcommon/lldependencies.cpp b/indra/llcommon/lldependencies.cpp new file mode 100644 index 0000000000..ffb5cfbdaa --- /dev/null +++ b/indra/llcommon/lldependencies.cpp @@ -0,0 +1,86 @@ +/** + * @file lldependencies.cpp + * @author Nat Goodspeed + * @date 2008-09-17 + * @brief Implementation for lldependencies. + * + * $LicenseInfo:firstyear=2008&license=viewergpl$ + * Copyright (c) 2008, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "lldependencies.h" +// STL headers +#include <map> +#include <sstream> +// std headers +// external library headers +#include <boost/graph/graph_traits.hpp> // for boost::graph_traits +#include <boost/graph/adjacency_list.hpp> +#include <boost/graph/topological_sort.hpp> +#include <boost/graph/exception.hpp> +// other Linden headers + +LLDependenciesBase::VertexList LLDependenciesBase::topo_sort(int vertices, const EdgeList& edges) const +{ + // Construct a Boost Graph Library graph according to the constraints + // we've collected. It seems as though we ought to be able to capture + // the uniqueness of vertex keys using a setS of vertices with a + // string property -- but I don't yet understand adjacency_list well + // enough to get there. All the examples I've seen so far use integers + // for vertices. + // Define the Graph type. Use a vector for vertices so we can use the + // default topological_sort vertex lookup by int index. Use a set for + // edges because the same dependency may be stated twice: Node "a" may + // specify that it must precede "b", while "b" may also state that it + // must follow "a". + typedef boost::adjacency_list<boost::setS, boost::vecS, boost::directedS, + boost::no_property> Graph; + // Instantiate the graph. Without vertex properties, we need say no + // more about vertices than the total number. + Graph g(edges.begin(), edges.end(), vertices); + // topo sort + typedef boost::graph_traits<Graph>::vertex_descriptor VertexDesc; + typedef std::vector<VertexDesc> SortedList; + SortedList sorted; + // note that it throws not_a_dag if it finds a cycle + try + { + boost::topological_sort(g, std::back_inserter(sorted)); + } + catch (const boost::not_a_dag& e) + { + // translate to the exception we define + std::ostringstream out; + out << "LLDependencies cycle: " << e.what() << '\n'; + // Omit independent nodes: display only those that might contribute to + // the cycle. + describe(out, false); + throw Cycle(out.str()); + } + // A peculiarity of boost::topological_sort() is that it emits results in + // REVERSE topological order: to get the result you want, you must + // traverse the SortedList using reverse iterators. + return VertexList(sorted.rbegin(), sorted.rend()); +} + +std::ostream& LLDependenciesBase::describe(std::ostream& out, bool full) const +{ + // Should never encounter this base-class implementation; may mean that + // the KEY type doesn't have a suitable ostream operator<<(). + out << "<no description available>"; + return out; +} + +std::string LLDependenciesBase::describe(bool full) const +{ + // Just use the ostream-based describe() on a std::ostringstream. The + // implementation is here mostly so that we can avoid #include <sstream> + // in the header file. + std::ostringstream out; + describe(out, full); + return out.str(); +} diff --git a/indra/llcommon/lldependencies.h b/indra/llcommon/lldependencies.h new file mode 100644 index 0000000000..bd4bd7c96a --- /dev/null +++ b/indra/llcommon/lldependencies.h @@ -0,0 +1,779 @@ +/** + * @file lldependencies.h + * @author Nat Goodspeed + * @date 2008-09-17 + * @brief LLDependencies: a generic mechanism for expressing "b must follow a, + * but precede c" + * + * $LicenseInfo:firstyear=2008&license=viewergpl$ + * Copyright (c) 2008, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_LLDEPENDENCIES_H) +#define LL_LLDEPENDENCIES_H + +#include <string> +#include <vector> +#include <set> +#include <map> +#include <stdexcept> +#include <iosfwd> +#include <boost/iterator/transform_iterator.hpp> +#include <boost/iterator/indirect_iterator.hpp> +#include <boost/range/iterator_range.hpp> +#include <boost/function.hpp> +#include <boost/bind.hpp> + +/***************************************************************************** +* Utilities +*****************************************************************************/ +/** + * generic range transformer: given a range acceptable to Boost.Range (e.g. a + * standard container, an iterator pair, ...) and a unary function to apply to + * each element of the range, make a corresponding range that lazily applies + * that function to each element on dereferencing. + */ +template<typename FUNCTION, typename RANGE> +inline +boost::iterator_range<boost::transform_iterator<FUNCTION, + typename boost::range_const_iterator<RANGE>::type> > +make_transform_range(const RANGE& range, FUNCTION function) +{ + // shorthand for the iterator type embedded in our return type + typedef boost::transform_iterator<FUNCTION, typename boost::range_const_iterator<RANGE>::type> + transform_iterator; + return boost::make_iterator_range(transform_iterator(boost::begin(range), function), + transform_iterator(boost::end(range), function)); +} + +/// non-const version of make_transform_range() +template<typename FUNCTION, typename RANGE> +inline +boost::iterator_range<boost::transform_iterator<FUNCTION, + typename boost::range_iterator<RANGE>::type> > +make_transform_range(RANGE& range, FUNCTION function) +{ + // shorthand for the iterator type embedded in our return type + typedef boost::transform_iterator<FUNCTION, typename boost::range_iterator<RANGE>::type> + transform_iterator; + return boost::make_iterator_range(transform_iterator(boost::begin(range), function), + transform_iterator(boost::end(range), function)); +} + +/** + * From any range compatible with Boost.Range, instantiate any class capable + * of accepting an iterator pair. + */ +template<class TYPE> +struct instance_from_range: public TYPE +{ + template<typename RANGE> + instance_from_range(RANGE range): + TYPE(boost::begin(range), boost::end(range)) + {} +}; + +/***************************************************************************** +* LLDependencies +*****************************************************************************/ +/** + * LLDependencies components that should not be reinstantiated for each KEY, + * NODE specialization + */ +class LLDependenciesBase +{ +public: + virtual ~LLDependenciesBase() {} + + /** + * Exception thrown by sort() if there's a cycle + */ + struct Cycle: public std::runtime_error + { + Cycle(const std::string& what): std::runtime_error(what) {} + }; + + /** + * Provide a short description of this LLDependencies instance on the + * specified output stream, assuming that its KEY type has an operator<<() + * that works with std::ostream. + * + * Pass @a full as @c false to omit any keys without dependency constraints. + */ + virtual std::ostream& describe(std::ostream& out, bool full=true) const; + + /// describe() to a string + virtual std::string describe(bool full=true) const; + +protected: + typedef std::vector< std::pair<int, int> > EdgeList; + typedef std::vector<int> VertexList; + VertexList topo_sort(int vertices, const EdgeList& edges) const; + + /** + * refpair is specifically intended to capture a pair of references. This + * is better than std::pair<T1&, T2&> because some implementations of + * std::pair's ctor accept const references to the two types. If the + * types are themselves references, this results in an illegal reference- + * to-reference. + */ + template<typename T1, typename T2> + struct refpair + { + refpair(T1 value1, T2 value2): + first(value1), + second(value2) + {} + T1 first; + T2 second; + }; +}; + +/// describe() helper: for most types, report the type as usual +template<typename T> +inline +std::ostream& LLDependencies_describe(std::ostream& out, const T& key) +{ + out << key; + return out; +} + +/// specialize LLDependencies_describe() for std::string +template<> +inline +std::ostream& LLDependencies_describe(std::ostream& out, const std::string& key) +{ + out << '"' << key << '"'; + return out; +} + +/** + * It's reasonable to use LLDependencies in a keys-only way, more or less like + * std::set. For that case, the default NODE type is an empty struct. + */ +struct LLDependenciesEmpty +{ + LLDependenciesEmpty() {} + /** + * Give it a constructor accepting void* so caller can pass placeholder + * values such as NULL or 0 rather than having to write + * LLDependenciesEmpty(). + */ + LLDependenciesEmpty(void*) {} +}; + +/** + * This class manages abstract dependencies between node types of your + * choosing. As with a std::map, nodes are copied when add()ed, so the node + * type should be relatively lightweight; to manipulate dependencies between + * expensive objects, use a pointer type. + * + * For a given node, you may state the keys of nodes that must precede it + * and/or nodes that must follow it. The sort() method will produce an order + * that should work, or throw an exception if the constraints are impossible. + * We cache results to minimize the cost of repeated sort() calls. + */ +template<typename KEY = std::string, + typename NODE = LLDependenciesEmpty> +class LLDependencies: public LLDependenciesBase +{ + typedef LLDependencies<KEY, NODE> self_type; + + /** + * Internally, we bundle the client's NODE with its before/after keys. + */ + struct DepNode + { + typedef std::set<KEY> dep_set; + DepNode(const NODE& node_, const dep_set& after_, const dep_set& before_): + node(node_), + after(after_), + before(before_) + {} + NODE node; + dep_set after, before; + }; + typedef std::map<KEY, DepNode> DepNodeMap; + typedef typename DepNodeMap::value_type DepNodeMapEntry; + + /// We have various ways to get the dependencies for a given DepNode. + /// Rather than having to restate each one for 'after' and 'before' + /// separately, pass a dep_selector so we can apply each to either. + typedef boost::function<const typename DepNode::dep_set&(const DepNode&)> dep_selector; + +public: + LLDependencies() {} + + typedef KEY key_type; + typedef NODE node_type; + + /// param type used to express lists of other node keys -- note that such + /// lists can be initialized with boost::assign::list_of() + typedef std::vector<KEY> KeyList; + + /** + * Add a new node. State its dependencies on other nodes (which may not + * yet have been added) by listing the keys of nodes this new one must + * follow, and separately the keys of nodes this new one must precede. + * + * The node you pass is @em copied into an internal data structure. If you + * want to modify the node value after add()ing it, capture the returned + * NODE& reference. + * + * @note + * Actual dependency analysis is deferred to the sort() method, so + * you can add an arbitrary number of nodes without incurring analysis + * overhead for each. The flip side of this is that add()ing nodes that + * define a cycle leaves this object in a state in which sort() will + * always throw the Cycle exception. + * + * Two distinct use cases are anticipated: + * * The data used to load this object are completely known at compile + * time (e.g. LLEventPump listener names). A Cycle exception represents a + * bug which can be corrected by the coder. The program need neither catch + * Cycle nor attempt to salvage the state of this object. + * * The data are loaded at runtime, therefore the universe of + * dependencies cannot be known at compile time. The client code should + * catch Cycle. + * ** If a Cycle exception indicates fatally-flawed input data, this + * object can simply be discarded, possibly with the entire program run. + * ** If it is essential to restore this object to a working state, the + * simplest workaround is to remove() nodes in LIFO order. + * *** It may be useful to add functionality to this class to track the + * add() chronology, providing a pop() method to remove the most recently + * added node. + * *** It may further be useful to add a restore() method which would + * pop() until sort() no longer throws Cycle. This method would be + * expensive -- but it's not clear that client code could resolve the + * problem more cheaply. + */ + NODE& add(const KEY& key, const NODE& node = NODE(), + const KeyList& after = KeyList(), + const KeyList& before = KeyList()) + { + // Get the passed-in lists as sets for equality comparison + typename DepNode::dep_set + after_set(after.begin(), after.end()), + before_set(before.begin(), before.end()); + // Try to insert the new node; if it already exists, find the old + // node instead. + std::pair<typename DepNodeMap::iterator, bool> inserted = + mNodes.insert(typename DepNodeMap::value_type(key, + DepNode(node, after_set, before_set))); + if (! inserted.second) // bool indicating success of insert() + { + // We already have a node by this name. Have its dependencies + // changed? If the existing node's dependencies are identical, the + // result will be unchanged, so we can leave the cache intact. + // Regardless of inserted.second, inserted.first is the iterator + // to the newly-inserted (or existing) map entry. Of course, that + // entry's second is the DepNode of interest. + if (inserted.first->second.after != after_set || + inserted.first->second.before != before_set) + { + // Dependencies have changed: clear the cached result. + mCache.clear(); + // save the new dependencies + inserted.first->second.after = after_set; + inserted.first->second.before = before_set; + } + } + else // this node is new + { + // This will change results. + mCache.clear(); + } + return inserted.first->second.node; + } + + /// the value of an iterator, showing both KEY and its NODE + typedef refpair<const KEY&, NODE&> value_type; + /// the value of a const_iterator + typedef refpair<const KEY&, const NODE&> const_value_type; + +private: + // Extract functors + static value_type value_extract(DepNodeMapEntry& entry) + { + return value_type(entry.first, entry.second.node); + } + + static const_value_type const_value_extract(const DepNodeMapEntry& entry) + { + return const_value_type(entry.first, entry.second.node); + } + + // All the iterator access methods return iterator ranges just to cut down + // on the friggin' boilerplate!! + + /// generic mNodes range method + template<typename ITERATOR, typename FUNCTION> + boost::iterator_range<ITERATOR> generic_range(FUNCTION function) + { + return make_transform_range(mNodes, function); + } + + /// generic mNodes const range method + template<typename ITERATOR, typename FUNCTION> + boost::iterator_range<ITERATOR> generic_range(FUNCTION function) const + { + return make_transform_range(mNodes, function); + } + +public: + /// iterator over value_type entries + typedef boost::transform_iterator<boost::function<value_type(DepNodeMapEntry&)>, + typename DepNodeMap::iterator> iterator; + /// range over value_type entries + typedef boost::iterator_range<iterator> range; + + /// iterate over value_type <i>in @c KEY order</i> rather than dependency order + range get_range() + { + return generic_range<iterator>(value_extract); + } + + /// iterator over const_value_type entries + typedef boost::transform_iterator<boost::function<const_value_type(const DepNodeMapEntry&)>, + typename DepNodeMap::const_iterator> const_iterator; + /// range over const_value_type entries + typedef boost::iterator_range<const_iterator> const_range; + + /// iterate over const_value_type <i>in @c KEY order</i> rather than dependency order + const_range get_range() const + { + return generic_range<const_iterator>(const_value_extract); + } + + /// iterator over stored NODEs + typedef boost::transform_iterator<boost::function<NODE&(DepNodeMapEntry&)>, + typename DepNodeMap::iterator> node_iterator; + /// range over stored NODEs + typedef boost::iterator_range<node_iterator> node_range; + + /// iterate over NODE <i>in @c KEY order</i> rather than dependency order + node_range get_node_range() + { + // First take a DepNodeMapEntry and extract a reference to its + // DepNode, then from that extract a reference to its NODE. + return generic_range<node_iterator>( + boost::bind<NODE&>(&DepNode::node, + boost::bind<DepNode&>(&DepNodeMapEntry::second, _1))); + } + + /// const iterator over stored NODEs + typedef boost::transform_iterator<boost::function<const NODE&(const DepNodeMapEntry&)>, + typename DepNodeMap::const_iterator> const_node_iterator; + /// const range over stored NODEs + typedef boost::iterator_range<const_node_iterator> const_node_range; + + /// iterate over const NODE <i>in @c KEY order</i> rather than dependency order + const_node_range get_node_range() const + { + // First take a DepNodeMapEntry and extract a reference to its + // DepNode, then from that extract a reference to its NODE. + return generic_range<const_node_iterator>( + boost::bind<const NODE&>(&DepNode::node, + boost::bind<const DepNode&>(&DepNodeMapEntry::second, _1))); + } + + /// const iterator over stored KEYs + typedef boost::transform_iterator<boost::function<const KEY&(const DepNodeMapEntry&)>, + typename DepNodeMap::const_iterator> const_key_iterator; + /// const range over stored KEYs + typedef boost::iterator_range<const_key_iterator> const_key_range; + // We don't provide a non-const iterator over KEYs because they should be + // immutable, and in fact our underlying std::map won't give us non-const + // references. + + /// iterate over const KEY <i>in @c KEY order</i> rather than dependency order + const_key_range get_key_range() const + { + // From a DepNodeMapEntry, extract a reference to its KEY. + return generic_range<const_key_iterator>( + boost::bind<const KEY&>(&DepNodeMapEntry::first, _1)); + } + + /** + * Find an existing NODE, or return NULL. We decided to avoid providing a + * method analogous to std::map::find(), for a couple of reasons: + * + * * For a find-by-key, getting back an iterator to the (key, value) pair + * is less than useful, since you already have the key in hand. + * * For a failed request, comparing to end() is problematic. First, we + * provide range accessors, so it's more code to get end(). Second, we + * provide a number of different ranges -- quick, to which one's end() + * should we compare the iterator returned by find()? + * + * The returned pointer is solely to allow expressing the not-found + * condition. LLDependencies still owns the found NODE. + */ + const NODE* get(const KEY& key) const + { + typename DepNodeMap::const_iterator found = mNodes.find(key); + if (found != mNodes.end()) + { + return &found->second.node; + } + return NULL; + } + + /** + * non-const get() + */ + NODE* get(const KEY& key) + { + // Use const implementation, then cast away const-ness of return + return const_cast<NODE*>(const_cast<const self_type*>(this)->get(key)); + } + + /** + * Remove a node with specified key. This operation is the major reason + * we rebuild the graph on the fly instead of storing it. + */ + bool remove(const KEY& key) + { + typename DepNodeMap::iterator found = mNodes.find(key); + if (found != mNodes.end()) + { + mNodes.erase(found); + return true; + } + return false; + } + +private: + /// cached list of iterators + typedef std::vector<iterator> iterator_list; + typedef typename iterator_list::iterator iterator_list_iterator; + +public: + /** + * The return type of the sort() method needs some explanation. Provide a + * public typedef to facilitate storing the result. + * + * * We will prepare mCache by looking up DepNodeMap iterators. + * * We want to return a range containing iterators that will walk mCache. + * * If we simply stored DepNodeMap iterators and returned + * (mCache.begin(), mCache.end()), dereferencing each iterator would + * obtain a DepNodeMap iterator. + * * We want the caller to loop over @c value_type: pair<KEY, NODE>. + * * This requires two transformations: + * ** mCache must contain @c LLDependencies::iterator so that + * dereferencing each entry will obtain an @c LLDependencies::value_type + * rather than a DepNodeMapEntry. + * ** We must wrap mCache's iterators in boost::indirect_iterator so that + * dereferencing one of our returned iterators will also dereference the + * iterator contained in mCache. + */ + typedef boost::iterator_range<boost::indirect_iterator<iterator_list_iterator> > sorted_range; + /// for convenience in looping over a sorted_range + typedef typename sorted_range::iterator sorted_iterator; + + /** + * Once we've loaded in the dependencies of interest, arrange them into an + * order that works -- or throw Cycle exception. + * + * Return an iterator range over (key, node) pairs that traverses them in + * the desired order. + */ + sorted_range sort() const + { + // Changes to mNodes cause us to clear our cache, so empty mCache + // means it's invalid and should be recomputed. However, if mNodes is + // also empty, then an empty mCache represents a valid order, so don't + // bother sorting. + if (mCache.empty() && ! mNodes.empty()) + { + // Construct a map of node keys to distinct vertex numbers -- even for + // nodes mentioned only in before/after constraints, that haven't yet + // been explicitly added. Rely on std::map rejecting a second attempt + // to insert the same key. Use the map's size() as the vertex number + // to get a distinct value for each successful insertion. + typedef std::map<KEY, int> VertexMap; + VertexMap vmap; + // Nest each of these loops because !@#$%? MSVC warns us that its + // former broken behavior has finally been fixed -- and our builds + // treat warnings as errors. + { + for (typename DepNodeMap::const_iterator nmi = mNodes.begin(), nmend = mNodes.end(); + nmi != nmend; ++nmi) + { + vmap.insert(typename VertexMap::value_type(nmi->first, vmap.size())); + for (typename DepNode::dep_set::const_iterator ai = nmi->second.after.begin(), + aend = nmi->second.after.end(); + ai != aend; ++ai) + { + vmap.insert(typename VertexMap::value_type(*ai, vmap.size())); + } + for (typename DepNode::dep_set::const_iterator bi = nmi->second.before.begin(), + bend = nmi->second.before.end(); + bi != bend; ++bi) + { + vmap.insert(typename VertexMap::value_type(*bi, vmap.size())); + } + } + } + // Define the edges. For this we must traverse mNodes again, mapping + // all the known key dependencies to integer pairs. + EdgeList edges; + { + for (typename DepNodeMap::const_iterator nmi = mNodes.begin(), nmend = mNodes.end(); + nmi != nmend; ++nmi) + { + int thisnode = vmap[nmi->first]; + // after dependencies: build edges from the named node to this one + for (typename DepNode::dep_set::const_iterator ai = nmi->second.after.begin(), + aend = nmi->second.after.end(); + ai != aend; ++ai) + { + edges.push_back(EdgeList::value_type(vmap[*ai], thisnode)); + } + // before dependencies: build edges from this node to the + // named one + for (typename DepNode::dep_set::const_iterator bi = nmi->second.before.begin(), + bend = nmi->second.before.end(); + bi != bend; ++bi) + { + edges.push_back(EdgeList::value_type(thisnode, vmap[*bi])); + } + } + } + // Hide the gory details of our topological sort, since they shouldn't + // get reinstantiated for each distinct NODE type. + VertexList sorted(topo_sort(vmap.size(), edges)); + // Build the reverse of vmap to look up the key for each vertex + // descriptor. vmap contains exactly one entry for each distinct key, + // and we're certain that the associated int values are distinct + // indexes. The fact that they're not in order is irrelevant. + KeyList vkeys(vmap.size()); + for (typename VertexMap::const_iterator vmi = vmap.begin(), vmend = vmap.end(); + vmi != vmend; ++vmi) + { + vkeys[vmi->second] = vmi->first; + } + // Walk the sorted output list, building the result into mCache so + // we'll have it next time someone asks. + mCache.clear(); + for (VertexList::const_iterator svi = sorted.begin(), svend = sorted.end(); + svi != svend; ++svi) + { + // We're certain that vkeys[*svi] exists. However, there might not + // yet be a corresponding entry in mNodes. + self_type* non_const_this(const_cast<self_type*>(this)); + typename DepNodeMap::iterator found = non_const_this->mNodes.find(vkeys[*svi]); + if (found != non_const_this->mNodes.end()) + { + // Make an iterator of appropriate type. + mCache.push_back(iterator(found, value_extract)); + } + } + } + // Whether or not we've just recomputed mCache, it should now contain + // the results we want. Return a range of indirect_iterators over it + // so that dereferencing a returned iterator will dereference the + // iterator stored in mCache and directly reference the (key, node) + // pair. + boost::indirect_iterator<iterator_list_iterator> + begin(mCache.begin()), + end(mCache.end()); + return sorted_range(begin, end); + } + + /// Override base-class describe() with actual implementation + virtual std::ostream& describe(std::ostream& out, bool full=true) const + { + typename DepNodeMap::const_iterator dmi(mNodes.begin()), dmend(mNodes.end()); + if (dmi != dmend) + { + std::string sep; + describe(out, sep, *dmi, full); + while (++dmi != dmend) + { + describe(out, sep, *dmi, full); + } + } + return out; + } + + /// describe() helper: report a DepNodeEntry + static std::ostream& describe(std::ostream& out, std::string& sep, + const DepNodeMapEntry& entry, bool full) + { + // If we were asked for a full report, describe every node regardless + // of whether it has dependencies. If we were asked to suppress + // independent nodes, describe this one if either after or before is + // non-empty. + if (full || (! entry.second.after.empty()) || (! entry.second.before.empty())) + { + out << sep; + sep = "\n"; + if (! entry.second.after.empty()) + { + out << "after "; + describe(out, entry.second.after); + out << " -> "; + } + LLDependencies_describe(out, entry.first); + if (! entry.second.before.empty()) + { + out << " -> before "; + describe(out, entry.second.before); + } + } + return out; + } + + /// describe() helper: report a dep_set + static std::ostream& describe(std::ostream& out, const typename DepNode::dep_set& keys) + { + out << '('; + typename DepNode::dep_set::const_iterator ki(keys.begin()), kend(keys.end()); + if (ki != kend) + { + LLDependencies_describe(out, *ki); + while (++ki != kend) + { + out << ", "; + LLDependencies_describe(out, *ki); + } + } + out << ')'; + return out; + } + + /// Iterator over the before/after KEYs on which a given NODE depends + typedef typename DepNode::dep_set::const_iterator dep_iterator; + /// range over the before/after KEYs on which a given NODE depends + typedef boost::iterator_range<dep_iterator> dep_range; + + /// dependencies access from key + dep_range get_dep_range_from_key(const KEY& key, const dep_selector& selector) const + { + typename DepNodeMap::const_iterator found = mNodes.find(key); + if (found != mNodes.end()) + { + return dep_range(selector(found->second)); + } + // We want to return an empty range. On some platforms a default- + // constructed range (e.g. dep_range()) does NOT suffice! The client + // is likely to try to iterate from boost::begin(range) to + // boost::end(range); yet these iterators might not be valid. Instead + // return a range over a valid, empty container. + static const typename DepNode::dep_set empty_deps; + return dep_range(empty_deps.begin(), empty_deps.end()); + } + + /// dependencies access from any one of our key-order iterators + template<typename ITERATOR> + dep_range get_dep_range_from_xform(const ITERATOR& iterator, const dep_selector& selector) const + { + return dep_range(selector(iterator.base()->second)); + } + + /// dependencies access from sorted_iterator + dep_range get_dep_range_from_sorted(const sorted_iterator& sortiter, + const dep_selector& selector) const + { + // sorted_iterator is a boost::indirect_iterator wrapping an mCache + // iterator, which we can obtain by sortiter.base(). Deferencing that + // gets us an mCache entry, an 'iterator' -- one of our traversal + // iterators -- on which we can use get_dep_range_from_xform(). + return get_dep_range_from_xform(*sortiter.base(), selector); + } + + /** + * Get a range over the after KEYs stored for the passed KEY or iterator, + * in <i>arbitrary order.</i> If you pass a nonexistent KEY, returns empty + * range -- same as a KEY with no after KEYs. Detect existence of a KEY + * using get() instead. + */ + template<typename KEY_OR_ITER> + dep_range get_after_range(const KEY_OR_ITER& key) const; + + /** + * Get a range over the before KEYs stored for the passed KEY or iterator, + * in <i>arbitrary order.</i> If you pass a nonexistent KEY, returns empty + * range -- same as a KEY with no before KEYs. Detect existence of a KEY + * using get() instead. + */ + template<typename KEY_OR_ITER> + dep_range get_before_range(const KEY_OR_ITER& key) const; + +private: + DepNodeMap mNodes; + mutable iterator_list mCache; +}; + +/** + * Functor to get a dep_range from a KEY or iterator -- generic case. If the + * passed value isn't one of our iterator specializations, assume it's + * convertible to the KEY type. + */ +template<typename KEY_ITER> +struct LLDependencies_dep_range_from +{ + template<typename KEY, typename NODE, typename SELECTOR> + typename LLDependencies<KEY, NODE>::dep_range + operator()(const LLDependencies<KEY, NODE>& deps, + const KEY_ITER& key, + const SELECTOR& selector) + { + return deps.get_dep_range_from_key(key, selector); + } +}; + +/// Specialize LLDependencies_dep_range_from for our key-order iterators +template<typename FUNCTION, typename ITERATOR> +struct LLDependencies_dep_range_from< boost::transform_iterator<FUNCTION, ITERATOR> > +{ + template<typename KEY, typename NODE, typename SELECTOR> + typename LLDependencies<KEY, NODE>::dep_range + operator()(const LLDependencies<KEY, NODE>& deps, + const boost::transform_iterator<FUNCTION, ITERATOR>& iter, + const SELECTOR& selector) + { + return deps.get_dep_range_from_xform(iter, selector); + } +}; + +/// Specialize LLDependencies_dep_range_from for sorted_iterator +template<typename BASEITER> +struct LLDependencies_dep_range_from< boost::indirect_iterator<BASEITER> > +{ + template<typename KEY, typename NODE, typename SELECTOR> + typename LLDependencies<KEY, NODE>::dep_range + operator()(const LLDependencies<KEY, NODE>& deps, + const boost::indirect_iterator<BASEITER>& iter, + const SELECTOR& selector) + { + return deps.get_dep_range_from_sorted(iter, selector); + } +}; + +/// generic get_after_range() implementation +template<typename KEY, typename NODE> +template<typename KEY_OR_ITER> +typename LLDependencies<KEY, NODE>::dep_range +LLDependencies<KEY, NODE>::get_after_range(const KEY_OR_ITER& key_iter) const +{ + return LLDependencies_dep_range_from<KEY_OR_ITER>()( + *this, + key_iter, + boost::bind<const typename DepNode::dep_set&>(&DepNode::after, _1)); +} + +/// generic get_before_range() implementation +template<typename KEY, typename NODE> +template<typename KEY_OR_ITER> +typename LLDependencies<KEY, NODE>::dep_range +LLDependencies<KEY, NODE>::get_before_range(const KEY_OR_ITER& key_iter) const +{ + return LLDependencies_dep_range_from<KEY_OR_ITER>()( + *this, + key_iter, + boost::bind<const typename DepNode::dep_set&>(&DepNode::before, _1)); +} + +#endif /* ! defined(LL_LLDEPENDENCIES_H) */ diff --git a/indra/llcommon/llerror.cpp b/indra/llcommon/llerror.cpp index b135dafb3c..8102eddb18 100644 --- a/indra/llcommon/llerror.cpp +++ b/indra/llcommon/llerror.cpp @@ -433,7 +433,7 @@ namespace LLError Settings() : printLocation(false), defaultLevel(LLError::LEVEL_DEBUG), - crashFunction(NULL), + crashFunction(), timeFunction(NULL), fileRecorder(NULL), fixedBufferRecorder(NULL), @@ -601,12 +601,18 @@ namespace LLError s.printLocation = print; } - void setFatalFunction(FatalFunction f) + void setFatalFunction(const FatalFunction& f) { Settings& s = Settings::get(); s.crashFunction = f; } + FatalFunction getFatalFunction() + { + Settings& s = Settings::get(); + return s.crashFunction; + } + void setTimeFunction(TimeFunction f) { Settings& s = Settings::get(); diff --git a/indra/llcommon/llerrorcontrol.h b/indra/llcommon/llerrorcontrol.h index a55d706d2e..c9424f8a5e 100644 --- a/indra/llcommon/llerrorcontrol.h +++ b/indra/llcommon/llerrorcontrol.h @@ -35,7 +35,7 @@ #define LL_LLERRORCONTROL_H #include "llerror.h" - +#include "boost/function.hpp" #include <string> class LLFixedBuffer; @@ -83,16 +83,38 @@ namespace LLError Control functions. */ - typedef void(*FatalFunction)(const std::string& message); + typedef boost::function<void(const std::string&)> FatalFunction; void crashAndLoop(const std::string& message); - // Default fatal funtion: access null pointer and loops forever + // Default fatal function: access null pointer and loops forever - void setFatalFunction(FatalFunction); + void setFatalFunction(const FatalFunction&); // The fatal function will be called when an message of LEVEL_ERROR // is logged. Note: supressing a LEVEL_ERROR message from being logged // (by, for example, setting a class level to LEVEL_NONE), will keep // the that message from causing the fatal funciton to be invoked. - + + FatalFunction getFatalFunction(); + // Retrieve the previously-set FatalFunction + + /// temporarily override the FatalFunction for the duration of a + /// particular scope, e.g. for unit tests + class OverrideFatalFunction + { + public: + OverrideFatalFunction(const FatalFunction& func): + mPrev(getFatalFunction()) + { + setFatalFunction(func); + } + ~OverrideFatalFunction() + { + setFatalFunction(mPrev); + } + + private: + FatalFunction mPrev; + }; + typedef std::string (*TimeFunction)(); std::string utcTime(); diff --git a/indra/llcommon/llevent.cpp b/indra/llcommon/llevent.cpp index 24be6e8b34..f669d0e13f 100644 --- a/indra/llcommon/llevent.cpp +++ b/indra/llcommon/llevent.cpp @@ -34,6 +34,8 @@ #include "llevent.h" +using namespace LLOldEvents; + /************************************************ Events ************************************************/ diff --git a/indra/llcommon/llevent.h b/indra/llcommon/llevent.h index 60887a060a..a74ddbd091 100644 --- a/indra/llcommon/llevent.h +++ b/indra/llcommon/llevent.h @@ -38,6 +38,9 @@ #include "llmemory.h" #include "llthread.h" +namespace LLOldEvents +{ + class LLEventListener; class LLEvent; class LLEventDispatcher; @@ -194,4 +197,6 @@ public: LLSD mValue; }; +} // LLOldEvents + #endif // LL_EVENT_H diff --git a/indra/llcommon/llevents.cpp b/indra/llcommon/llevents.cpp new file mode 100644 index 0000000000..eb380ba7c8 --- /dev/null +++ b/indra/llcommon/llevents.cpp @@ -0,0 +1,501 @@ +/** + * @file llevents.cpp + * @author Nat Goodspeed + * @date 2008-09-12 + * @brief Implementation for llevents. + * + * $LicenseInfo:firstyear=2008&license=viewergpl$ + * Copyright (c) 2008, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" + +#if LL_WINDOWS +#pragma warning (disable : 4675) // "resolved by ADL" -- just as I want! +#endif + +// associated header +#include "llevents.h" +// STL headers +#include <set> +#include <sstream> +#include <algorithm> +// std headers +#include <typeinfo> +#include <cassert> +#include <cmath> +#include <cctype> +// external library headers +#include <boost/range/iterator_range.hpp> +#if LL_WINDOWS +#pragma warning (push) +#pragma warning (disable : 4701) // compiler thinks might use uninitialized var, but no +#endif +#include <boost/lexical_cast.hpp> +#if LL_WINDOWS +#pragma warning (pop) +#endif +// other Linden headers + +/***************************************************************************** +* queue_names: specify LLEventPump names that should be instantiated as +* LLEventQueue +*****************************************************************************/ +/** + * At present, we recognize particular requested LLEventPump names as needing + * LLEventQueues. Later on we'll migrate this information to an external + * configuration file. + */ +const char* queue_names[] = +{ + "placeholder - replace with first real name string" +}; + +/***************************************************************************** +* If there's a "mainloop" pump, listen on that to flush all LLEventQueues +*****************************************************************************/ +struct RegisterFlush +{ + RegisterFlush(): + pumps(LLEventPumps::instance()), + mainloop(pumps.obtain("mainloop")), + name("flushLLEventQueues") + { + mainloop.listen(name, boost::bind(&RegisterFlush::flush, this, _1)); + } + bool flush(const LLSD&) + { + pumps.flush(); + return false; + } + ~RegisterFlush() + { + mainloop.stopListening(name); + } + LLEventPumps& pumps; + LLEventPump& mainloop; + const std::string name; +}; +static RegisterFlush registerFlush; + +/***************************************************************************** +* LLEventPumps +*****************************************************************************/ +LLEventPumps::LLEventPumps(): + // Until we migrate this information to an external config file, + // initialize mQueueNames from the static queue_names array. + mQueueNames(boost::begin(queue_names), boost::end(queue_names)) +{ +} + +LLEventPump& LLEventPumps::obtain(const std::string& name) +{ + PumpMap::iterator found = mPumpMap.find(name); + if (found != mPumpMap.end()) + { + // Here we already have an LLEventPump instance with the requested + // name. + return *found->second; + } + // Here we must instantiate an LLEventPump subclass. + LLEventPump* newInstance; + // Should this name be an LLEventQueue? + PumpNames::const_iterator nfound = mQueueNames.find(name); + if (nfound != mQueueNames.end()) + newInstance = new LLEventQueue(name); + else + newInstance = new LLEventStream(name); + // LLEventPump's constructor implicitly registers each new instance in + // mPumpMap. But remember that we instantiated it (in mOurPumps) so we'll + // delete it later. + mOurPumps.insert(newInstance); + return *newInstance; +} + +void LLEventPumps::flush() +{ + // Flush every known LLEventPump instance. Leave it up to each instance to + // decide what to do with the flush() call. + for (PumpMap::iterator pmi = mPumpMap.begin(), pmend = mPumpMap.end(); pmi != pmend; ++pmi) + { + pmi->second->flush(); + } +} + +std::string LLEventPumps::registerNew(const LLEventPump& pump, const std::string& name, bool tweak) +{ + std::pair<PumpMap::iterator, bool> inserted = + mPumpMap.insert(PumpMap::value_type(name, const_cast<LLEventPump*>(&pump))); + // If the insert worked, then the name is unique; return that. + if (inserted.second) + return name; + // Here the new entry was NOT inserted, and therefore name isn't unique. + // Unless we're permitted to tweak it, that's Bad. + if (! tweak) + { + throw LLEventPump::DupPumpName(std::string("Duplicate LLEventPump name '") + name + "'"); + } + // The passed name isn't unique, but we're permitted to tweak it. Find the + // first decimal-integer suffix not already taken. The insert() attempt + // above will have set inserted.first to the iterator of the existing + // entry by that name. Starting there, walk forward until we reach an + // entry that doesn't start with 'name'. For each entry consisting of name + // + integer suffix, capture the integer suffix in a set. Use a set + // because we're going to encounter string suffixes in the order: name1, + // name10, name11, name2, ... Walking those possibilities in that order + // isn't convenient to detect the first available "hole." + std::set<int> suffixes; + PumpMap::iterator pmi(inserted.first), pmend(mPumpMap.end()); + // We already know inserted.first references the existing entry with + // 'name' as the key; skip that one and start with the next. + while (++pmi != pmend) + { + if (pmi->first.substr(0, name.length()) != name) + { + // Found the first entry beyond the entries starting with 'name': + // stop looping. + break; + } + // Here we're looking at an entry that starts with 'name'. Is the rest + // of it an integer? + // Dubious (?) assumption: in the local character set, decimal digits + // are in increasing order such that '9' is the last of them. This + // test deals with 'name' values such as 'a', where there might be a + // very large number of entries starting with 'a' whose suffixes + // aren't integers. A secondary assumption is that digit characters + // precede most common name characters (true in ASCII, false in + // EBCDIC). The test below is correct either way, but it's worth more + // if the assumption holds. + if (pmi->first[name.length()] > '9') + break; + // It should be cheaper to detect that we're not looking at a digit + // character -- and therefore the suffix can't possibly be an integer + // -- than to attempt the lexical_cast and catch the exception. + if (! std::isdigit(pmi->first[name.length()])) + continue; + // Okay, the first character of the suffix is a digit, it's worth at + // least attempting to convert to int. + try + { + suffixes.insert(boost::lexical_cast<int>(pmi->first.substr(name.length()))); + } + catch (const boost::bad_lexical_cast&) + { + // If the rest of pmi->first isn't an int, just ignore it. + } + } + // Here we've accumulated in 'suffixes' all existing int suffixes of the + // entries starting with 'name'. Find the first unused one. + int suffix = 1; + for ( ; suffixes.find(suffix) != suffixes.end(); ++suffix) + ; + // Here 'suffix' is not in 'suffixes'. Construct a new name based on that + // suffix, insert it and return it. + std::ostringstream out; + out << name << suffix; + return registerNew(pump, out.str(), tweak); +} + +void LLEventPumps::unregister(const LLEventPump& pump) +{ + // Remove this instance from mPumpMap + PumpMap::iterator found = mPumpMap.find(pump.getName()); + if (found != mPumpMap.end()) + { + mPumpMap.erase(found); + } + // If this instance is one we created, also remove it from mOurPumps so we + // won't try again to delete it later! + PumpSet::iterator psfound = mOurPumps.find(const_cast<LLEventPump*>(&pump)); + if (psfound != mOurPumps.end()) + { + mOurPumps.erase(psfound); + } +} + +LLEventPumps::~LLEventPumps() +{ + // On destruction, delete every LLEventPump we instantiated (via + // obtain()). CAREFUL: deleting an LLEventPump calls its destructor, which + // calls unregister(), which removes that LLEventPump instance from + // mOurPumps. So an iterator loop over mOurPumps to delete contained + // LLEventPump instances is dangerous! Instead, delete them one at a time + // until mOurPumps is empty. + while (! mOurPumps.empty()) + { + delete *mOurPumps.begin(); + } +} + +/***************************************************************************** +* LLEventPump +*****************************************************************************/ +#if LL_WINDOWS +#pragma warning (push) +#pragma warning (disable : 4355) // 'this' used in initializer list: yes, intentionally +#endif + +LLEventPump::LLEventPump(const std::string& name, bool tweak): + // Register every new instance with LLEventPumps + mName(LLEventPumps::instance().registerNew(*this, name, tweak)), + mEnabled(true) +{} + +#if LL_WINDOWS +#pragma warning (pop) +#endif + +LLEventPump::~LLEventPump() +{ + // Unregister this doomed instance from LLEventPumps + LLEventPumps::instance().unregister(*this); +} + +// static data member +const LLEventPump::NameList LLEventPump::empty; + +LLBoundListener LLEventPump::listen_impl(const std::string& name, const LLEventListener& listener, + const NameList& after, + const NameList& before) +{ + // Check for duplicate name before connecting listener to mSignal + ConnectionMap::const_iterator found = mConnections.find(name); + // In some cases the user might disconnect a connection explicitly -- or + // might use LLEventTrackable to disconnect implicitly. Either way, we can + // end up retaining in mConnections a zombie connection object that's + // already been disconnected. Such a connection object can't be + // reconnected -- nor, in the case of LLEventTrackable, would we want to + // try, since disconnection happens with the destruction of the listener + // object. That means it's safe to overwrite a disconnected connection + // object with the new one we're attempting. The case we want to prevent + // is only when the existing connection object is still connected. + if (found != mConnections.end() && found->second.connected()) + { + throw DupListenerName(std::string("Attempt to register duplicate listener name '") + name + + "' on " + typeid(*this).name() + " '" + getName() + "'"); + } + // Okay, name is unique, try to reconcile its dependencies. Specify a new + // "node" value that we never use for an mSignal placement; we'll fix it + // later. + DependencyMap::node_type& newNode = mDeps.add(name, -1.0, after, before); + // What if this listener has been added, removed and re-added? In that + // case newNode already has a non-negative value because we never remove a + // listener from mDeps. But keep processing uniformly anyway in case the + // listener was added back with different dependencies. Then mDeps.sort() + // would put it in a different position, and the old newNode placement + // value would be wrong, so we'd have to reassign it anyway. Trust that + // re-adding a listener with the same dependencies is the trivial case for + // mDeps.sort(): it can just replay its cache. + DependencyMap::sorted_range sorted_range; + try + { + // Can we pick an order that works including this new entry? + sorted_range = mDeps.sort(); + } + catch (const DependencyMap::Cycle& e) + { + // No: the new node's after/before dependencies have made mDeps + // unsortable. If we leave the new node in mDeps, it will continue + // to screw up all future attempts to sort()! Pull it out. + mDeps.remove(name); + throw Cycle(std::string("New listener '") + name + "' on " + typeid(*this).name() + + " '" + getName() + "' would cause cycle: " + e.what()); + } + // Walk the list to verify that we haven't changed the order. + float previous = 0.0, myprev = 0.0; + DependencyMap::sorted_iterator mydmi = sorted_range.end(); // need this visible after loop + for (DependencyMap::sorted_iterator dmi = sorted_range.begin(); + dmi != sorted_range.end(); ++dmi) + { + // Since we've added the new entry with an invalid placement, + // recognize it and skip it. + if (dmi->first == name) + { + // Remember the iterator belonging to our new node, and which + // placement value was 'previous' at that point. + mydmi = dmi; + myprev = previous; + continue; + } + // If the new node has rearranged the existing nodes, we'll find + // that their placement values are no longer in increasing order. + if (dmi->second < previous) + { + // This is another scenario in which we'd better back out the + // newly-added node from mDeps -- but don't do it yet, we want to + // traverse the existing mDeps to report on it! + // Describe the change to the order of our listeners. Copy + // everything but the newest listener to a vector we can sort to + // obtain the old order. + typedef std::vector< std::pair<float, std::string> > SortNameList; + SortNameList sortnames; + for (DependencyMap::sorted_iterator cdmi(sorted_range.begin()), cdmend(sorted_range.end()); + cdmi != cdmend; ++cdmi) + { + if (cdmi->first != name) + { + sortnames.push_back(SortNameList::value_type(cdmi->second, cdmi->first)); + } + } + std::sort(sortnames.begin(), sortnames.end()); + std::ostringstream out; + out << "New listener '" << name << "' on " << typeid(*this).name() << " '" << getName() + << "' would move previous listener '" << dmi->first << "'\nwas: "; + SortNameList::const_iterator sni(sortnames.begin()), snend(sortnames.end()); + if (sni != snend) + { + out << sni->second; + while (++sni != snend) + { + out << ", " << sni->second; + } + } + out << "\nnow: "; + DependencyMap::sorted_iterator ddmi(sorted_range.begin()), ddmend(sorted_range.end()); + if (ddmi != ddmend) + { + out << ddmi->first; + while (++ddmi != ddmend) + { + out << ", " << ddmi->first; + } + } + // NOW remove the offending listener node. + mDeps.remove(name); + // Having constructed a description of the order change, inform caller. + throw OrderChange(out.str()); + } + // This node becomes the previous one. + previous = dmi->second; + } + // We just got done with a successful mDeps.add(name, ...) call. We'd + // better have found 'name' somewhere in that sorted list! + assert(mydmi != sorted_range.end()); + // Four cases: + // 0. name is the only entry: placement 1.0 + // 1. name is the first of several entries: placement (next placement)/2 + // 2. name is between two other entries: placement (myprev + (next placement))/2 + // 3. name is the last entry: placement ceil(myprev) + 1.0 + // Since we've cleverly arranged for myprev to be 0.0 if name is the + // first entry, this folds down to two cases. Case 1 is subsumed by + // case 2, and case 0 is subsumed by case 3. So we need only handle + // cases 2 and 3, which means we need only detect whether name is the + // last entry. Increment mydmi to see if there's anything beyond. + if (++mydmi != sorted_range.end()) + { + // The new node isn't last. Place it between the previous node and + // the successor. + newNode = (myprev + mydmi->second)/2.0; + } + else + { + // The new node is last. Bump myprev up to the next integer, add + // 1.0 and use that. + newNode = std::ceil(myprev) + 1.0; + } + // Now that newNode has a value that places it appropriately in mSignal, + // connect it. + LLBoundListener bound = mSignal.connect(newNode, listener); + mConnections[name] = bound; + return bound; +} + +LLBoundListener LLEventPump::getListener(const std::string& name) const +{ + ConnectionMap::const_iterator found = mConnections.find(name); + if (found != mConnections.end()) + { + return found->second; + } + // not found, return dummy LLBoundListener + return LLBoundListener(); +} + +void LLEventPump::stopListening(const std::string& name) +{ + ConnectionMap::iterator found = mConnections.find(name); + if (found != mConnections.end()) + { + found->second.disconnect(); + mConnections.erase(found); + } + // We intentionally do NOT remove this name from mDeps. It may happen that + // the same listener with the same name and dependencies will jump on and + // off this LLEventPump repeatedly. Keeping a cache of dependencies will + // avoid a new dependency sort in such cases. +} + +/***************************************************************************** +* LLEventStream +*****************************************************************************/ +bool LLEventStream::post(const LLSD& event) +{ + if (! mEnabled) + return false; + // Let caller know if any one listener handled the event. This is mostly + // useful when using LLEventStream as a listener for an upstream + // LLEventPump. + return mSignal(event); +} + +/***************************************************************************** +* LLEventQueue +*****************************************************************************/ +bool LLEventQueue::post(const LLSD& event) +{ + if (mEnabled) + { + // Defer sending this event by queueing it until flush() + mEventQueue.push_back(event); + } + // Unconditionally return false. We won't know until flush() whether a + // listener claims to have handled the event -- meanwhile, don't block + // other listeners. + return false; +} + +void LLEventQueue::flush() +{ + // Consider the case when a given listener on this LLEventQueue posts yet + // another event on the same queue. If we loop over mEventQueue directly, + // we'll end up processing all those events during the same flush() call + // -- rather like an EventStream. Instead, copy mEventQueue and clear it, + // so that any new events posted to this LLEventQueue during flush() will + // be processed in the *next* flush() call. + EventQueue queue(mEventQueue); + mEventQueue.clear(); + for ( ; ! queue.empty(); queue.pop_front()) + { + mSignal(queue.front()); + } +} + +/***************************************************************************** +* LLListenerOrPumpName +*****************************************************************************/ +LLListenerOrPumpName::LLListenerOrPumpName(const std::string& pumpname): + // Look up the specified pumpname, and bind its post() method as our listener + mListener(boost::bind(&LLEventPump::post, + boost::ref(LLEventPumps::instance().obtain(pumpname)), + _1)) +{ +} + +LLListenerOrPumpName::LLListenerOrPumpName(const char* pumpname): + // Look up the specified pumpname, and bind its post() method as our listener + mListener(boost::bind(&LLEventPump::post, + boost::ref(LLEventPumps::instance().obtain(pumpname)), + _1)) +{ +} + +bool LLListenerOrPumpName::operator()(const LLSD& event) const +{ + if (! mListener) + { + throw Empty("attempting to call uninitialized"); + } + return (*mListener)(event); +} diff --git a/indra/llcommon/llevents.h b/indra/llcommon/llevents.h new file mode 100644 index 0000000000..f70d532e4c --- /dev/null +++ b/indra/llcommon/llevents.h @@ -0,0 +1,822 @@ +/** + * @file llevents.h + * @author Kent Quirk, Nat Goodspeed + * @date 2008-09-11 + * @brief This is an implementation of the event system described at + * https://wiki.lindenlab.com/wiki/Viewer:Messaging/Event_System, + * originally introduced in llnotifications.h. It has nothing + * whatsoever to do with the older system in llevent.h. + * + * $LicenseInfo:firstyear=2008&license=viewergpl$ + * Copyright (c) 2008, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_LLEVENTS_H) +#define LL_LLEVENTS_H + +#include <string> +#include <map> +#include <set> +#include <vector> +#include <list> +#include <deque> +#include <stdexcept> +#include <boost/signals2.hpp> +#include <boost/bind.hpp> +#include <boost/shared_ptr.hpp> +#include <boost/enable_shared_from_this.hpp> +#include <boost/utility.hpp> // noncopyable +#include <boost/optional/optional.hpp> +#include <boost/ptr_container/ptr_vector.hpp> +#include <boost/visit_each.hpp> +#include <boost/ref.hpp> // reference_wrapper +#include <boost/type_traits/is_pointer.hpp> +#include <boost/utility/addressof.hpp> +#include <boost/preprocessor/repetition/enum_params.hpp> +#include <boost/preprocessor/iteration/local.hpp> +#include <boost/function.hpp> +#include <boost/static_assert.hpp> +#include "llsd.h" +#include "llmemory.h" +#include "lldependencies.h" + +// override this to allow binding free functions with more parameters +#ifndef LLEVENTS_LISTENER_ARITY +#define LLEVENTS_LISTENER_ARITY 10 +#endif + +// hack for testing +#ifndef testable +#define testable private +#endif + +/***************************************************************************** +* Signal and handler declarations +* Using a single handler signature means that we can have a common handler +* type, rather than needing a distinct one for each different handler. +*****************************************************************************/ + +/** + * A boost::signals Combiner that stops the first time a handler returns true + * We need this because we want to have our handlers return bool, so that + * we have the option to cause a handler to stop further processing. The + * default handler fails when the signal returns a value but has no slots. + */ +struct LLStopWhenHandled +{ + typedef bool result_type; + + template<typename InputIterator> + result_type operator()(InputIterator first, InputIterator last) const + { + for (InputIterator si = first; si != last; ++si) + { + if (*si) + { + return true; + } + } + return false; + } +}; + +/** + * We want to have a standard signature for all signals; this way, + * we can easily document a protocol for communicating across + * dlls and into scripting languages someday. + * + * We want to return a bool to indicate whether the signal has been + * handled and should NOT be passed on to other listeners. + * Return true to stop further handling of the signal, and false + * to continue. + * + * We take an LLSD because this way the contents of the signal + * are independent of the API used to communicate it. + * It is const ref because then there's low cost to pass it; + * if you only need to inspect it, it's very cheap. + * + * @internal + * The @c float template parameter indicates that we will internally use @c + * float to indicate relative listener order on a given LLStandardSignal. + * Don't worry, the @c float values are strictly internal! They are not part + * of the interface, for the excellent reason that requiring the caller to + * specify a numeric key to establish order means that the caller must know + * the universe of possible values. We use LLDependencies for that instead. + */ +typedef boost::signals2::signal<bool(const LLSD&), LLStopWhenHandled, float> LLStandardSignal; +/// Methods that forward listeners (e.g. constructed with +/// <tt>boost::bind()</tt>) should accept (const LLEventListener&) +typedef LLStandardSignal::slot_type LLEventListener; +/// Result of registering a listener, supports <tt>connected()</tt>, +/// <tt>disconnect()</tt> and <tt>blocked()</tt> +typedef boost::signals2::connection LLBoundListener; + +/** + * A common idiom for event-based code is to accept either a callable -- + * directly called on completion -- or the string name of an LLEventPump on + * which to post the completion event. Specifying a parameter as <tt>const + * LLListenerOrPumpName&</tt> allows either. + * + * Calling a validly-constructed LLListenerOrPumpName, passing the LLSD + * 'event' object, either calls the callable or posts the event to the named + * LLEventPump. + * + * A default-constructed LLListenerOrPumpName is 'empty'. (This is useful as + * the default value of an optional method parameter.) Calling it throws + * LLListenerOrPumpName::Empty. Test for this condition beforehand using + * either <tt>if (param)</tt> or <tt>if (! param)</tt>. + */ +class LLListenerOrPumpName +{ +public: + /// passing string name of LLEventPump + LLListenerOrPumpName(const std::string& pumpname); + /// passing string literal (overload so compiler isn't forced to infer + /// double conversion) + LLListenerOrPumpName(const char* pumpname); + /// passing listener -- the "anything else" catch-all case. The type of an + /// object constructed by boost::bind() isn't intended to be written out. + /// Normally we'd just accept 'const LLEventListener&', but that would + /// require double implicit conversion: boost::bind() object to + /// LLEventListener, LLEventListener to LLListenerOrPumpName. So use a + /// template to forward anything. + template<typename T> + LLListenerOrPumpName(const T& listener): mListener(listener) {} + + /// for omitted method parameter: uninitialized mListener + LLListenerOrPumpName() {} + + /// test for validity + operator bool() const { return bool(mListener); } + bool operator! () const { return ! mListener; } + + /// explicit accessor + const LLEventListener& getListener() const { return *mListener; } + + /// implicit conversion to LLEventListener + operator LLEventListener() const { return *mListener; } + + /// allow calling directly + bool operator()(const LLSD& event) const; + + /// exception if you try to call when empty + struct Empty: public std::runtime_error + { + Empty(const std::string& what): + std::runtime_error(std::string("LLListenerOrPumpName::Empty: ") + what) {} + }; + +private: + boost::optional<LLEventListener> mListener; +}; + +/***************************************************************************** +* LLEventPumps +*****************************************************************************/ +class LLEventPump; + +/** + * LLEventPumps is a Singleton manager through which one typically accesses + * this subsystem. + */ +class LLEventPumps: public LLSingleton<LLEventPumps> +{ + friend class LLSingleton<LLEventPumps>; +public: + /** + * Find or create an LLEventPump instance with a specific name. We return + * a reference so there's no question about ownership. obtain() @em finds + * an instance without conferring @em ownership. + */ + LLEventPump& obtain(const std::string& name); + /** + * Flush all known LLEventPump instances + */ + void flush(); + +private: + friend class LLEventPump; + /** + * Register a new LLEventPump instance (internal) + */ + std::string registerNew(const LLEventPump&, const std::string& name, bool tweak); + /** + * Unregister a doomed LLEventPump instance (internal) + */ + void unregister(const LLEventPump&); + +private: + LLEventPumps(); + ~LLEventPumps(); + +testable: + // Map of all known LLEventPump instances, whether or not we instantiated + // them. We store a plain old LLEventPump* because this map doesn't claim + // ownership of the instances. Though the common usage pattern is to + // request an instance using obtain(), it's fair to instantiate an + // LLEventPump subclass statically, as a class member, on the stack or on + // the heap. In such cases, the instantiating party is responsible for its + // lifespan. + typedef std::map<std::string, LLEventPump*> PumpMap; + PumpMap mPumpMap; + // Set of all LLEventPumps we instantiated. Membership in this set means + // we claim ownership, and will delete them when this LLEventPumps is + // destroyed. + typedef std::set<LLEventPump*> PumpSet; + PumpSet mOurPumps; + // LLEventPump names that should be instantiated as LLEventQueue rather + // than as LLEventStream + typedef std::set<std::string> PumpNames; + PumpNames mQueueNames; +}; + +/***************************************************************************** +* details +*****************************************************************************/ +namespace LLEventDetail +{ + /// Any callable capable of connecting an LLEventListener to an + /// LLStandardSignal to produce an LLBoundListener can be mapped to this + /// signature. + typedef boost::function<LLBoundListener(const LLEventListener&)> ConnectFunc; + + /** + * Utility template function to use Visitor appropriately + * + * @param listener Callable to connect, typically a boost::bind() + * expression. This will be visited by Visitor using boost::visit_each(). + * @param connect_func Callable that will connect() @a listener to an + * LLStandardSignal, returning LLBoundListener. + */ + template <typename LISTENER> + LLBoundListener visit_and_connect(const LISTENER& listener, + const ConnectFunc& connect_func); +} // namespace LLEventDetail + +/***************************************************************************** +* LLEventPump +*****************************************************************************/ +/** + * LLEventPump is the base class interface through which we access the + * concrete subclasses LLEventStream and LLEventQueue. + */ +class LLEventPump: boost::noncopyable +{ +public: + /** + * Exception thrown by LLEventPump(). You are trying to instantiate an + * LLEventPump (subclass) using the same name as some other instance, and + * you didn't pass <tt>tweak=true</tt> to permit it to generate a unique + * variant. + */ + struct DupPumpName: public std::runtime_error + { + DupPumpName(const std::string& what): + std::runtime_error(std::string("DupPumpName: ") + what) {} + }; + + /** + * Instantiate an LLEventPump (subclass) with the string name by which it + * can be found using LLEventPumps::obtain(). + * + * If you pass (or default) @a tweak to @c false, then a duplicate name + * will throw DupPumpName. This won't happen if LLEventPumps::obtain() + * instantiates the LLEventPump, because obtain() uses find-or-create + * logic. It can only happen if you instantiate an LLEventPump in your own + * code -- and a collision with the name of some other LLEventPump is + * likely to cause much more subtle problems! + * + * When you hand-instantiate an LLEventPump, consider passing @a tweak as + * @c true. This directs LLEventPump() to append a suffix to the passed @a + * name to make it unique. You can retrieve the adjusted name by calling + * getName() on your new instance. + */ + LLEventPump(const std::string& name, bool tweak=false); + virtual ~LLEventPump(); + + /// group exceptions thrown by listen(). We use exceptions because these + /// particular errors are likely to be coding errors, found and fixed by + /// the developer even before preliminary checkin. + struct ListenError: public std::runtime_error + { + ListenError(const std::string& what): std::runtime_error(what) {} + }; + /** + * exception thrown by listen(). You are attempting to register a + * listener on this LLEventPump using the same listener name as an + * already-registered listener. + */ + struct DupListenerName: public ListenError + { + DupListenerName(const std::string& what): + ListenError(std::string("DupListenerName: ") + what) + {} + }; + /** + * exception thrown by listen(). The order dependencies specified for your + * listener are incompatible with existing listeners. + * + * Consider listener "a" which specifies before "b" and "b" which + * specifies before "c". You are now attempting to register "c" before + * "a". There is no order that can satisfy all constraints. + */ + struct Cycle: public ListenError + { + Cycle(const std::string& what): ListenError(std::string("Cycle: ") + what) {} + }; + /** + * exception thrown by listen(). This one means that your new listener + * would force a change to the order of previously-registered listeners, + * and we don't have a good way to implement that. + * + * Consider listeners "some", "other" and "third". "some" and "other" are + * registered earlier without specifying relative order, so "other" + * happens to be first. Now you attempt to register "third" after "some" + * and before "other". Whoops, that would require swapping "some" and + * "other", which we can't do. Instead we throw this exception. + * + * It may not be possible to change the registration order so we already + * know "third"s order requirement by the time we register the second of + * "some" and "other". A solution would be to specify that "some" must + * come before "other", or equivalently that "other" must come after + * "some". + */ + struct OrderChange: public ListenError + { + OrderChange(const std::string& what): ListenError(std::string("OrderChange: ") + what) {} + }; + + /// used by listen() + typedef std::vector<std::string> NameList; + /// convenience placeholder for when you explicitly want to pass an empty + /// NameList + const static NameList empty; + + /// Get this LLEventPump's name + std::string getName() const { return mName; } + + /** + * Register a new listener with a unique name. Specify an optional list + * of other listener names after which this one must be called, likewise + * an optional list of other listener names before which this one must be + * called. The other listeners mentioned need not yet be registered + * themselves. listen() can throw any ListenError; see ListenError + * subclasses. + * + * If (as is typical) you pass a <tt>boost::bind()</tt> expression, + * listen() will inspect the components of that expression. If a bound + * object matches any of several cases, the connection will automatically + * be disconnected when that object is destroyed. + * + * * You bind a <tt>boost::weak_ptr</tt>. + * * Binding a <tt>boost::shared_ptr</tt> that way would ensure that the + * referenced object would @em never be destroyed, since the @c + * shared_ptr stored in the LLEventPump would remain an outstanding + * reference. Use the weaken() function to convert your @c shared_ptr to + * @c weak_ptr. Because this is easy to forget, binding a @c shared_ptr + * will produce a compile error (@c BOOST_STATIC_ASSERT failure). + * * You bind a simple pointer or reference to an object derived from + * <tt>boost::enable_shared_from_this</tt>. (UNDER CONSTRUCTION) + * * You bind a simple pointer or reference to an object derived from + * LLEventTrackable. Unlike the cases described above, though, this is + * vulnerable to a couple of cross-thread race conditions, as described + * in the LLEventTrackable documentation. + */ + template <typename LISTENER> + LLBoundListener listen(const std::string& name, const LISTENER& listener, + const NameList& after=NameList(), + const NameList& before=NameList()) + { + // Examine listener, using our listen_impl() method to make the + // actual connection. + // This is why listen() is a template. Conversion from boost::bind() + // to LLEventListener performs type erasure, so it's important to look + // at the boost::bind object itself before that happens. + return LLEventDetail::visit_and_connect(listener, + boost::bind(&LLEventPump::listen_impl, + this, + name, + _1, + after, + before)); + } + + /// Get the LLBoundListener associated with the passed name (dummy + /// LLBoundListener if not found) + virtual LLBoundListener getListener(const std::string& name) const; + /** + * Instantiate one of these to block an existing connection: + * @code + * { // in some local scope + * LLEventPump::Blocker block(someLLBoundListener); + * // code that needs the connection blocked + * } // unblock the connection again + * @endcode + */ + typedef boost::signals2::shared_connection_block Blocker; + /// Unregister a listener by name. Prefer this to + /// <tt>getListener(name).disconnect()</tt> because stopListening() also + /// forgets this name. + virtual void stopListening(const std::string& name); + /// Post an event to all listeners. The @c bool return is only meaningful + /// if the underlying leaf class is LLEventStream -- beware of relying on + /// it too much! Truthfully, we return @c bool mostly to permit chaining + /// one LLEventPump as a listener on another. + virtual bool post(const LLSD&) = 0; + /// Enable/disable: while disabled, silently ignore all post() calls + virtual void enable(bool enabled=true) { mEnabled = enabled; } + /// query + virtual bool enabled() const { return mEnabled; } + +private: + friend class LLEventPumps; + /// flush queued events + virtual void flush() {} + +private: + virtual LLBoundListener listen_impl(const std::string& name, const LLEventListener&, + const NameList& after, + const NameList& before); + std::string mName; + +protected: + /// implement the dispatching + LLStandardSignal mSignal; + /// valve open? + bool mEnabled; + /// Map of named listeners. This tracks the listeners that actually exist + /// at this moment. When we stopListening(), we discard the entry from + /// this map. + typedef std::map<std::string, boost::signals2::connection> ConnectionMap; + ConnectionMap mConnections; + typedef LLDependencies<std::string, float> DependencyMap; + /// Dependencies between listeners. For each listener, track the float + /// used to establish its place in mSignal's order. This caches all the + /// listeners that have ever registered; stopListening() does not discard + /// the entry from this map. This is to avoid a new dependency sort if the + /// same listener with the same dependencies keeps hopping on and off this + /// LLEventPump. + DependencyMap mDeps; +}; + +/***************************************************************************** +* LLEventStream +*****************************************************************************/ +/** + * LLEventStream is a thin wrapper around LLStandardSignal. Posting an + * event immediately calls all registered listeners. + */ +class LLEventStream: public LLEventPump +{ +public: + LLEventStream(const std::string& name, bool tweak=false): LLEventPump(name, tweak) {} + virtual ~LLEventStream() {} + + /// Post an event to all listeners + virtual bool post(const LLSD& event); +}; + +/***************************************************************************** +* LLEventQueue +*****************************************************************************/ +/** + * LLEventQueue isa LLEventPump whose post() method defers calling registered + * listeners until flush() is called. + */ +class LLEventQueue: public LLEventPump +{ +public: + LLEventQueue(const std::string& name, bool tweak=false): LLEventPump(name, tweak) {} + virtual ~LLEventQueue() {} + + /// Post an event to all listeners + virtual bool post(const LLSD& event); + +private: + /// flush queued events + virtual void flush(); + +private: + typedef std::deque<LLSD> EventQueue; + EventQueue mEventQueue; +}; + +/***************************************************************************** +* LLEventTrackable and underpinnings +*****************************************************************************/ +/** + * LLEventTrackable wraps boost::signals2::trackable, which resembles + * boost::trackable. Derive your listener class from LLEventTrackable instead, + * and use something like + * <tt>LLEventPump::listen(boost::bind(&YourTrackableSubclass::method, + * instance, _1))</tt>. This will implicitly disconnect when the object + * referenced by @c instance is destroyed. + * + * @note + * LLEventTrackable doesn't address a couple of cases: + * * Object destroyed during call + * - You enter a slot call in thread A. + * - Thread B destroys the object, which of course disconnects it from any + * future slot calls. + * - Thread A's call uses 'this', which now refers to a defunct object. + * Undefined behavior results. + * * Call during destruction + * - @c MySubclass is derived from LLEventTrackable. + * - @c MySubclass registers one of its own methods using + * <tt>LLEventPump::listen()</tt>. + * - The @c MySubclass object begins destruction. <tt>~MySubclass()</tt> + * runs, destroying state specific to the subclass. (For instance, a + * <tt>Foo*</tt> data member is <tt>delete</tt>d but not zeroed.) + * - The listening method will not be disconnected until + * <tt>~LLEventTrackable()</tt> runs. + * - Before we get there, another thread posts data to the @c LLEventPump + * instance, calling the @c MySubclass method. + * - The method in question relies on valid @c MySubclass state. (For + * instance, it attempts to dereference the <tt>Foo*</tt> pointer that was + * <tt>delete</tt>d but not zeroed.) + * - Undefined behavior results. + * If you suspect you may encounter any such scenario, you're better off + * managing the lifespan of your object with <tt>boost::shared_ptr</tt>. + * Passing <tt>LLEventPump::listen()</tt> a <tt>boost::bind()</tt> expression + * involving a <tt>boost::weak_ptr<Foo></tt> is recognized specially, engaging + * thread-safe Boost.Signals2 machinery. + */ +typedef boost::signals2::trackable LLEventTrackable; + +/** + * We originally provided a suite of overloaded + * LLEventTrackable::listenTo(LLEventPump&, ...) methods that would call + * LLEventPump::listen(...) and then pass the returned LLBoundListener to + * LLEventTrackable::track(). This was workable but error-prone: the coder + * must remember to call listenTo() rather than the more straightforward + * listen() method. + * + * Now we publish only the single canonical listen() method, so there's a + * uniform mechanism. Having a single way to do this is good, in that there's + * no question in the coder's mind which of several alternatives to choose. + * + * To support automatic connection management, we use boost::visit_each + * (http://www.boost.org/doc/libs/1_37_0/doc/html/boost/visit_each.html) to + * inspect each argument of a boost::bind expression. (Although the visit_each + * mechanism was first introduced with the original Boost.Signals library, it + * was only later documented.) + * + * Cases: + * * At least one of the function's arguments is a boost::weak_ptr<T>. Pass + * the corresponding shared_ptr to slot_type::track(). Ideally that would be + * the object whose method we want to call, but in fact we do the same for + * any weak_ptr we might find among the bound arguments. If we're passing + * our bound method a weak_ptr to some object, wouldn't the destruction of + * that object invalidate the call? So we disconnect automatically when any + * such object is destroyed. This is the mechanism preferred by boost:: + * signals2. + * * One of the functions's arguments is a boost::shared_ptr<T>. This produces + * a compile error: the bound copy of the shared_ptr stored in the + * boost_bind object stored in the signal object would make the referenced + * T object immortal. We provide a weaken() function. Pass + * weaken(your_shared_ptr) instead. (We can inspect, but not modify, the + * boost::bind object. Otherwise we'd replace the shared_ptr with weak_ptr + * implicitly and just proceed.) + * * One of the function's arguments is a plain pointer/reference to an object + * derived from boost::enable_shared_from_this. We assume that this object + * is managed using boost::shared_ptr, so we implicitly extract a shared_ptr + * and track that. (UNDER CONSTRUCTION) + * * One of the function's arguments is derived from LLEventTrackable. Pass + * the LLBoundListener to its LLEventTrackable::track(). This is vulnerable + * to a couple different race conditions, as described in LLEventTrackable + * documentation. (NOTE: Now that LLEventTrackable is a typedef for + * boost::signals2::trackable, the Signals2 library handles this itself, so + * our visitor needs no special logic for this case.) + * * Any other argument type is irrelevant to automatic connection management. + */ + +namespace LLEventDetail +{ + template <typename F> + const F& unwrap(const F& f) { return f; } + + template <typename F> + const F& unwrap(const boost::reference_wrapper<F>& f) { return f.get(); } + + // Most of the following is lifted from the Boost.Signals use of + // visit_each. + template<bool Cond> struct truth {}; + + /** + * boost::visit_each() Visitor, used on a template argument <tt>const F& + * f</tt> as follows (see visit_and_connect()): + * @code + * LLEventListener listener(f); + * Visitor visitor(listener); // bind listener so it can track() shared_ptrs + * using boost::visit_each; // allow unqualified visit_each() call for ADL + * visit_each(visitor, unwrap(f)); + * @endcode + */ + class Visitor + { + public: + /** + * Visitor binds a reference to LLEventListener so we can track() any + * shared_ptrs we find in the argument list. + */ + Visitor(LLEventListener& listener): + mListener(listener) + { + } + + /** + * boost::visit_each() calls this method for each component of a + * boost::bind() expression. + */ + template <typename T> + void operator()(const T& t) const + { + decode(t, 0); + } + + private: + // decode() decides between a reference wrapper and anything else + // boost::ref() variant + template<typename T> + void decode(const boost::reference_wrapper<T>& t, int) const + { +// add_if_trackable(t.get_pointer()); + } + + // decode() anything else + template<typename T> + void decode(const T& t, long) const + { + typedef truth<(boost::is_pointer<T>::value)> is_a_pointer; + maybe_get_pointer(t, is_a_pointer()); + } + + // maybe_get_pointer() decides between a pointer and a non-pointer + // plain pointer variant + template<typename T> + void maybe_get_pointer(const T& t, truth<true>) const + { +// add_if_trackable(t); + } + + // shared_ptr variant + template<typename T> + void maybe_get_pointer(const boost::shared_ptr<T>& t, truth<false>) const + { + // If we have a shared_ptr to this object, it doesn't matter + // whether the object is derived from LLEventTrackable, so no + // further analysis of T is needed. +// mListener.track(t); + + // Make this case illegal. Passing a bound shared_ptr to + // slot_type::track() is useless, since the bound shared_ptr will + // keep the object alive anyway! Force the coder to cast to weak_ptr. + + // Trivial as it is, make the BOOST_STATIC_ASSERT() condition + // dependent on template param so the macro is only evaluated if + // this method is in fact instantiated, as described here: + // http://www.boost.org/doc/libs/1_34_1/doc/html/boost_staticassert.html + + // ATTENTION: Don't bind a shared_ptr<anything> using + // LLEventPump::listen(boost::bind()). Doing so captures a copy of + // the shared_ptr, making the referenced object effectively + // immortal. Use the weaken() function, e.g.: + // somepump.listen(boost::bind(...weaken(my_shared_ptr)...)); + // This lets us automatically disconnect when the referenced + // object is destroyed. + BOOST_STATIC_ASSERT(sizeof(T) == 0); + } + + // weak_ptr variant + template<typename T> + void maybe_get_pointer(const boost::weak_ptr<T>& t, truth<false>) const + { + // If we have a weak_ptr to this object, it doesn't matter + // whether the object is derived from LLEventTrackable, so no + // further analysis of T is needed. + mListener.track(t); +// std::cout << "Found weak_ptr<" << typeid(T).name() << ">!\n"; + } + +#if 0 + // reference to anything derived from boost::enable_shared_from_this + template <typename T> + inline void maybe_get_pointer(const boost::enable_shared_from_this<T>& ct, + truth<false>) const + { + // Use the slot_type::track(shared_ptr) mechanism. Cast away + // const-ness because (in our code base anyway) it's unusual + // to find shared_ptr<const T>. + boost::enable_shared_from_this<T>& + t(const_cast<boost::enable_shared_from_this<T>&>(ct)); + std::cout << "Capturing shared_from_this()" << std::endl; + boost::shared_ptr<T> sp(t.shared_from_this()); +/*==========================================================================*| + std::cout << "Capturing weak_ptr" << std::endl; + boost::weak_ptr<T> wp(sp); +|*==========================================================================*/ + std::cout << "Tracking shared__ptr" << std::endl; + mListener.track(sp); + } +#endif + + // non-pointer variant + template<typename T> + void maybe_get_pointer(const T& t, truth<false>) const + { + // Take the address of this object, because the object itself may be + // trackable +// add_if_trackable(boost::addressof(t)); + } + +/*==========================================================================*| + // add_if_trackable() adds LLEventTrackable objects to mTrackables + inline void add_if_trackable(const LLEventTrackable* t) const + { + if (t) + { + } + } + + // pointer to anything not an LLEventTrackable subclass + inline void add_if_trackable(const void*) const + { + } + + // pointer to free function + // The following construct uses the preprocessor to generate + // add_if_trackable() overloads accepting pointer-to-function taking + // 0, 1, ..., LLEVENTS_LISTENER_ARITY parameters of arbitrary type. +#define BOOST_PP_LOCAL_MACRO(n) \ + template <typename R \ + BOOST_PP_COMMA_IF(n) \ + BOOST_PP_ENUM_PARAMS(n, typename T)> \ + inline void \ + add_if_trackable(R (*)(BOOST_PP_ENUM_PARAMS(n, T))) const \ + { \ + } +#define BOOST_PP_LOCAL_LIMITS (0, LLEVENTS_LISTENER_ARITY) +#include BOOST_PP_LOCAL_ITERATE() +#undef BOOST_PP_LOCAL_MACRO +#undef BOOST_PP_LOCAL_LIMITS +|*==========================================================================*/ + + /// Bind a reference to the LLEventListener to call its track() method. + LLEventListener& mListener; + }; + + /** + * Utility template function to use Visitor appropriately + * + * @param raw_listener Callable to connect, typically a boost::bind() + * expression. This will be visited by Visitor using boost::visit_each(). + * @param connect_funct Callable that will connect() @a raw_listener to an + * LLStandardSignal, returning LLBoundListener. + */ + template <typename LISTENER> + LLBoundListener visit_and_connect(const LISTENER& raw_listener, + const ConnectFunc& connect_func) + { + // Capture the listener + LLEventListener listener(raw_listener); + // Define our Visitor, binding the listener so we can call + // listener.track() if we discover any shared_ptr<Foo>. + LLEventDetail::Visitor visitor(listener); + // Allow unqualified visit_each() call for ADL + using boost::visit_each; + // Visit each component of a boost::bind() expression. Pass + // 'raw_listener', our template argument, rather than 'listener' from + // which type details have been erased. unwrap() comes from + // Boost.Signals, in case we were passed a boost::ref(). + visit_each(visitor, LLEventDetail::unwrap(raw_listener)); + // Make the connection using passed function. At present, wrapping + // this functionality into this function is a bit silly: we don't + // really need a visit_and_connect() function any more, just a visit() + // function. The definition of this function dates from when, after + // visit_each(), after establishing the connection, we had to + // postprocess the new connection with the visitor object. That's no + // longer necessary. + return connect_func(listener); + } +} // namespace LLEventDetail + +// Somewhat to my surprise, passing boost::bind(...boost::weak_ptr<T>...) to +// listen() fails in Boost code trying to instantiate LLEventListener (i.e. +// LLStandardSignal::slot_type) because the boost::get_pointer() utility function isn't +// specialized for boost::weak_ptr. This remedies that omission. +namespace boost +{ + template <typename T> + T* get_pointer(const weak_ptr<T>& ptr) { return shared_ptr<T>(ptr).get(); } +} + +/// Since we forbid use of listen(boost::bind(...shared_ptr<T>...)), provide an +/// easy way to cast to the corresponding weak_ptr. +template <typename T> +boost::weak_ptr<T> weaken(const boost::shared_ptr<T>& ptr) +{ + return boost::weak_ptr<T>(ptr); +} + +#endif /* ! defined(LL_LLEVENTS_H) */ diff --git a/indra/llcommon/lllazy.cpp b/indra/llcommon/lllazy.cpp new file mode 100644 index 0000000000..215095bc27 --- /dev/null +++ b/indra/llcommon/lllazy.cpp @@ -0,0 +1,23 @@ +/** + * @file lllazy.cpp + * @author Nat Goodspeed + * @date 2009-01-28 + * @brief Implementation for lllazy. + * + * $LicenseInfo:firstyear=2009&license=viewergpl$ + * Copyright (c) 2009, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "lllazy.h" +// STL headers +// std headers +// external library headers +// other Linden headers + +// lllazy.h is presently header-only. This file exists only because our CMake +// test macro ADD_BUILD_TEST requires it. +int dummy = 0; diff --git a/indra/llcommon/lllazy.h b/indra/llcommon/lllazy.h new file mode 100644 index 0000000000..2240954d98 --- /dev/null +++ b/indra/llcommon/lllazy.h @@ -0,0 +1,382 @@ +/** + * @file lllazy.h + * @author Nat Goodspeed + * @date 2009-01-22 + * @brief Lazy instantiation of specified type. Useful in conjunction with + * Michael Feathers's "Extract and Override Getter" ("Working + * Effectively with Legacy Code", p. 352). + * + * Quoting his synopsis of steps on p.355: + * + * 1. Identify the object you need a getter for. + * 2. Extract all of the logic needed to create the object into a getter. + * 3. Replace all uses of the object with calls to the getter, and initialize + * the reference that holds the object to null in all constructors. + * 4. Add the first-time logic to the getter so that the object is constructed + * and assigned to the reference whenever the reference is null. + * 5. Subclass the class and override the getter to provide an alternative + * object for testing. + * + * It's the second half of bullet 3 (3b, as it were) that bothers me. I find + * it all too easy to imagine adding pointer initializers to all but one + * constructor... the one not exercised by my tests. That suggested using + * (e.g.) boost::scoped_ptr<MyObject> so you don't have to worry about + * destroying it either. + * + * However, introducing additional machinery allows us to encapsulate bullet 4 + * as well. + * + * $LicenseInfo:firstyear=2009&license=viewergpl$ + * Copyright (c) 2009, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_LLLAZY_H) +#define LL_LLLAZY_H + +#include <boost/function.hpp> +#include <boost/scoped_ptr.hpp> +#include <boost/lambda/construct.hpp> +#include <stdexcept> + +/// LLLazyCommon simply factors out of LLLazy<T> things that don't depend on +/// its template parameter. +class LLLazyCommon +{ +public: + /** + * This exception is thrown if you try to replace an LLLazy<T>'s factory + * (or T* instance) after it already has an instance in hand. Since T + * might well be stateful, we can't know the effect of silently discarding + * and replacing an existing instance, so we disallow it. This facility is + * intended for testing, and in a test scenario we can definitely control + * that. + */ + struct InstanceChange: public std::runtime_error + { + InstanceChange(const std::string& what): std::runtime_error(what) {} + }; + +protected: + /** + * InstanceChange might be appropriate in a couple of different LLLazy<T> + * methods. Factor out the common logic. + */ + template <typename PTR> + static void ensureNoInstance(const PTR& ptr) + { + if (ptr) + { + // Too late: we've already instantiated the lazy object. We don't + // know whether it's stateful or not, so it's not safe to discard + // the existing instance in favor of a replacement. + throw InstanceChange("Too late to replace LLLazy instance"); + } + } +}; + +/** + * LLLazy<T> is useful when you have an outer class Outer that you're trying + * to bring under unit test, that contains a data member difficult to + * instantiate in a test harness. Typically the data member's class Inner has + * many thorny dependencies. Feathers generally advocates "Extract and + * Override Factory Method" (p. 350). But in C++, you can't call a derived + * class override of a virtual method from the derived class constructor, + * which limits applicability of "Extract and Override Factory Method." For + * such cases Feathers presents "Extract and Override Getter" (p. 352). + * + * So we'll assume that your class Outer contains a member like this: + * @code + * Inner mInner; + * @endcode + * + * LLLazy<Inner> can be used to replace this member. You can directly declare: + * @code + * LLLazy<Inner> mInner; + * @endcode + * and change references to mInner accordingly. + * + * (Alternatively, you can add a base class of the form + * <tt>LLLazyBase<Inner></tt>. This is discussed further in the LLLazyBase<T> + * documentation.) + * + * LLLazy<T> binds a <tt>boost::scoped_ptr<T></tt> and a factory functor + * returning T*. You can either bind that functor explicitly or let it default + * to the expression <tt>new T()</tt>. + * + * As long as LLLazy<T> remains unreferenced, its T remains uninstantiated. + * The first time you use get(), <tt>operator*()</tt> or <tt>operator->()</tt> + * it will instantiate its T and thereafter behave like a pointer to it. + * + * Thus, any existing reference to <tt>mInner.member</tt> should be replaced + * with <tt>mInner->member</tt>. Any simple reference to @c mInner should be + * replaced by <tt>*mInner</tt>. + * + * (If the original declaration was a pointer initialized in Outer's + * constructor, e.g. <tt>Inner* mInner</tt>, so much the better. In that case + * you should be able to drop in <tt>LLLazy<Inner></tt> without much change.) + * + * The support for "Extract and Override Getter" lies in the fact that you can + * replace the factory functor -- or provide an explicit T*. Presumably this + * is most useful from a test subclass -- which suggests that your @c mInner + * member should be @c protected. + * + * Note that <tt>boost::lambda::new_ptr<T>()</tt> makes a dandy factory + * functor, for either the set() method or LLLazy<T>'s constructor. If your T + * requires constructor arguments, use an expression more like + * <tt>boost::lambda::bind(boost::lambda::new_ptr<T>(), arg1, arg2, ...)</tt>. + * + * Of course the point of replacing the functor is to substitute a class that, + * though referenced as Inner*, is not an Inner; presumably this is a testing + * subclass of Inner (e.g. TestInner). Thus your test subclass TestOuter for + * the containing class Outer will contain something like this: + * @code + * class TestOuter: public Outer + * { + * public: + * TestOuter() + * { + * // mInner must be 'protected' rather than 'private' + * mInner.set(boost::lambda::new_ptr<TestInner>()); + * } + * ... + * }; + * @endcode + */ +template <typename T> +class LLLazy: public LLLazyCommon +{ +public: + /// Any nullary functor returning T* will work as a Factory + typedef boost::function<T* ()> Factory; + + /// The default LLLazy constructor uses <tt>new T()</tt> as its Factory + LLLazy(): + mFactory(boost::lambda::new_ptr<T>()) + {} + + /// Bind an explicit Factory functor + LLLazy(const Factory& factory): + mFactory(factory) + {} + + /// Reference T, instantiating it if this is the first access + const T& get() const + { + if (! mInstance) + { + // use the bound Factory functor + mInstance.reset(mFactory()); + } + return *mInstance; + } + + /// non-const get() + T& get() + { + return const_cast<T&>(const_cast<const LLLazy<T>*>(this)->get()); + } + + /// operator*() is equivalent to get() + const T& operator*() const { return get(); } + /// operator*() is equivalent to get() + T& operator*() { return get(); } + + /** + * operator->() must return (something resembling) T*. It's tempting to + * return the underlying boost::scoped_ptr<T>, but that would require + * breaking out the lazy-instantiation logic from get() into a common + * private method. Assume the pointer used for operator->() access is very + * short-lived. + */ + const T* operator->() const { return &get(); } + /// non-const operator->() + T* operator->() { return &get(); } + + /// set(Factory). This will throw InstanceChange if mInstance has already + /// been set. + void set(const Factory& factory) + { + ensureNoInstance(mInstance); + mFactory = factory; + } + + /// set(T*). This will throw InstanceChange if mInstance has already been + /// set. + void set(T* instance) + { + ensureNoInstance(mInstance); + mInstance.reset(instance); + } + +private: + Factory mFactory; + // Consider an LLLazy<T> member of a class we're accessing by const + // reference. We want to allow even const methods to touch the LLLazy<T> + // member. Hence the actual pointer must be mutable because such access + // might assign it. + mutable boost::scoped_ptr<T> mInstance; +}; + +#if (! defined(__GNUC__)) || (__GNUC__ > 3) || (__GNUC__ == 3 && __GNUC_MINOR__ > 3) +// Not gcc at all, or a gcc more recent than gcc 3.3 +#define GCC33 0 +#else +#define GCC33 1 +#endif + +/** + * LLLazyBase<T> wraps LLLazy<T>, giving you an alternative way to replace + * <tt>Inner mInner;</tt>. Instead of coding <tt>LLLazy<Inner> mInner</tt>, + * you can add LLLazyBase<Inner> to your Outer class's bases, e.g.: + * @code + * class Outer: public LLLazyBase<Inner> + * { + * ... + * }; + * @endcode + * + * This gives you @c public get() and @c protected set() methods without + * having to make your LLLazy<Inner> member @c protected. The tradeoff is that + * you must access the wrapped LLLazy<Inner> using get() and set() rather than + * with <tt>operator*()</tt> or <tt>operator->()</tt>. + * + * This mechanism can be used for more than one member, but only if they're of + * different types. That is, you can replace: + * @code + * DifficultClass mDifficult; + * AwkwardType mAwkward; + * @endcode + * with: + * @code + * class Outer: public LLLazyBase<DifficultClass>, public LLLazyBase<AwkwardType> + * { + * ... + * }; + * @endcode + * but for a situation like this: + * @code + * DifficultClass mMainDifficult, mAuxDifficult; + * @endcode + * you should directly embed LLLazy<DifficultClass> (q.v.). + * + * For multiple LLLazyBase bases, e.g. the <tt>LLLazyBase<DifficultClass>, + * LLLazyBase<AwkwardType></tt> example above, access the relevant get()/set() + * as (e.g.) <tt>LLLazyBase<DifficultClass>::get()</tt>. (This is why you + * can't have multiple LLLazyBase<T> of the same T.) For a bit of syntactic + * sugar, please see getLazy()/setLazy(). + */ +template <typename T> +class LLLazyBase +{ +public: + /// invoke default LLLazy constructor + LLLazyBase() {} + /// make wrapped LLLazy bind an explicit Factory + LLLazyBase(const typename LLLazy<T>::Factory& factory): + mInstance(factory) + {} + + /// access to LLLazy::get() + T& get() { return *mInstance; } + /// access to LLLazy::get() + const T& get() const { return *mInstance; } + +protected: + // see getLazy()/setLazy() + #if (! GCC33) + template <typename T2, class MYCLASS> friend T2& getLazy(MYCLASS* this_); + template <typename T2, class MYCLASS> friend const T2& getLazy(const MYCLASS* this_); + #else // gcc 3.3 + template <typename T2, class MYCLASS> friend T2& getLazy(const MYCLASS* this_); + #endif // gcc 3.3 + template <typename T2, class MYCLASS> friend void setLazy(MYCLASS* this_, T2* instance); + template <typename T2, class MYCLASS> + friend void setLazy(MYCLASS* this_, const typename LLLazy<T2>::Factory& factory); + + /// access to LLLazy::set(Factory) + void set(const typename LLLazy<T>::Factory& factory) + { + mInstance.set(factory); + } + + /// access to LLLazy::set(T*) + void set(T* instance) + { + mInstance.set(instance); + } + +private: + LLLazy<T> mInstance; +}; + +/** + * @name getLazy()/setLazy() + * Suppose you have something like the following: + * @code + * class Outer: public LLLazyBase<DifficultClass>, public LLLazyBase<AwkwardType> + * { + * ... + * }; + * @endcode + * + * Your methods can reference the @c DifficultClass instance using + * <tt>LLLazyBase<DifficultClass>::get()</tt>, which is admittedly a bit ugly. + * Alternatively, you can write <tt>getLazy<DifficultClass>(this)</tt>, which + * is somewhat more straightforward to read. + * + * Similarly, + * @code + * LLLazyBase<DifficultClass>::set(new TestDifficultClass()); + * @endcode + * could instead be written: + * @code + * setLazy<DifficultClass>(this, new TestDifficultClass()); + * @endcode + * + * @note + * I wanted to provide getLazy() and setLazy() without explicitly passing @c + * this. That would imply making them methods on a base class rather than free + * functions. But if <tt>LLLazyBase<T></tt> derives normally from (say) @c + * LLLazyGrandBase providing those methods, then unqualified getLazy() would + * be ambiguous: you'd have to write <tt>LLLazyBase<T>::getLazy<T>()</tt>, + * which is even uglier than <tt>LLLazyBase<T>::get()</tt>, and therefore + * pointless. You can make the compiler not care which @c LLLazyGrandBase + * instance you're talking about by making @c LLLazyGrandBase a @c virtual + * base class of @c LLLazyBase. But in that case, + * <tt>LLLazyGrandBase::getLazy<T>()</tt> can't access + * <tt>LLLazyBase<T>::get()</tt>! + * + * We want <tt>getLazy<T>()</tt> to access <tt>LLLazyBase<T>::get()</tt> as if + * in the lexical context of some subclass method. Ironically, free functions + * let us do that better than methods on a @c virtual base class -- but that + * implies passing @c this explicitly. So be it. + */ +//@{ +#if (! GCC33) +template <typename T, class MYCLASS> +T& getLazy(MYCLASS* this_) { return this_->LLLazyBase<T>::get(); } +template <typename T, class MYCLASS> +const T& getLazy(const MYCLASS* this_) { return this_->LLLazyBase<T>::get(); } +#else // gcc 3.3 +// For const-correctness, we really should have two getLazy() variants: one +// accepting const MYCLASS* and returning const T&, the other accepting +// non-const MYCLASS* and returning non-const T&. This works fine on the Mac +// (gcc 4.0.1) and Windows (MSVC 8.0), but fails on our Linux 32-bit Debian +// Sarge stations (gcc 3.3.5). Since I really don't know how to beat that aging +// compiler over the head to make it do the right thing, I'm going to have to +// move forward with the wrong thing: a single getLazy() function that accepts +// const MYCLASS* and returns non-const T&. +template <typename T, class MYCLASS> +T& getLazy(const MYCLASS* this_) { return const_cast<MYCLASS*>(this_)->LLLazyBase<T>::get(); } +#endif // gcc 3.3 +template <typename T, class MYCLASS> +void setLazy(MYCLASS* this_, T* instance) { this_->LLLazyBase<T>::set(instance); } +template <typename T, class MYCLASS> +void setLazy(MYCLASS* this_, const typename LLLazy<T>::Factory& factory) +{ + this_->LLLazyBase<T>::set(factory); +} +//@} + +#endif /* ! defined(LL_LLLAZY_H) */ diff --git a/indra/llcommon/stringize.h b/indra/llcommon/stringize.h new file mode 100644 index 0000000000..1b2958020f --- /dev/null +++ b/indra/llcommon/stringize.h @@ -0,0 +1,75 @@ +/** + * @file stringize.h + * @author Nat Goodspeed + * @date 2008-12-17 + * @brief stringize(item) template function and STRINGIZE(expression) macro + * + * $LicenseInfo:firstyear=2008&license=viewergpl$ + * Copyright (c) 2008, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_STRINGIZE_H) +#define LL_STRINGIZE_H + +#include <sstream> + +/** + * stringize(item) encapsulates an idiom we use constantly, using + * operator<<(std::ostringstream&, TYPE) followed by std::ostringstream::str() + * to render a string expressing some item. + */ +template <typename T> +std::string stringize(const T& item) +{ + std::ostringstream out; + out << item; + return out.str(); +} + +/** + * STRINGIZE(item1 << item2 << item3 ...) effectively expands to the + * following: + * @code + * std::ostringstream out; + * out << item1 << item2 << item3 ... ; + * return out.str(); + * @endcode + */ +#define STRINGIZE(EXPRESSION) (static_cast<std::ostringstream&>(Stringize() << EXPRESSION).str()) + +/** + * Helper class for STRINGIZE() macro. Ideally the body of + * STRINGIZE(EXPRESSION) would look something like this: + * @code + * (std::ostringstream() << EXPRESSION).str() + * @endcode + * That doesn't work because each of the relevant operator<<() functions + * accepts a non-const std::ostream&, to which you can't pass a temp instance + * of std::ostringstream. Stringize plays the necessary const tricks to make + * the whole thing work. + */ +class Stringize +{ +public: + /** + * This is the essence of Stringize. The leftmost << operator (the one + * coded in the STRINGIZE() macro) engages this operator<<() const method + * on the temp Stringize instance. Every other << operator (ones embedded + * in EXPRESSION) simply sees the std::ostream& returned by the first one. + * + * Finally, the STRINGIZE() macro downcasts that std::ostream& to + * std::ostringstream&. + */ + template <typename T> + std::ostream& operator<<(const T& item) const + { + mOut << item; + return mOut; + } + +private: + mutable std::ostringstream mOut; +}; + +#endif /* ! defined(LL_STRINGIZE_H) */ diff --git a/indra/llcommon/tests/lllazy_test.cpp b/indra/llcommon/tests/lllazy_test.cpp new file mode 100644 index 0000000000..db581d650f --- /dev/null +++ b/indra/llcommon/tests/lllazy_test.cpp @@ -0,0 +1,227 @@ +/** + * @file lllazy_test.cpp + * @author Nat Goodspeed + * @date 2009-01-28 + * @brief Tests of lllazy.h. + * + * $LicenseInfo:firstyear=2009&license=viewergpl$ + * Copyright (c) 2009, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "lllazy.h" +// STL headers +#include <iostream> +// std headers +// external library headers +#include <boost/lambda/construct.hpp> +#include <boost/lambda/bind.hpp> +// other Linden headers +#include "../test/lltut.h" + +namespace bll = boost::lambda; + +/***************************************************************************** +* Test classes +*****************************************************************************/ + +// Let's say that because of its many external dependencies, YuckyFoo is very +// hard to instantiate in a test harness. +class YuckyFoo +{ +public: + virtual ~YuckyFoo() {} + virtual std::string whoami() const { return "YuckyFoo"; } +}; + +// Let's further suppose that YuckyBar is another hard-to-instantiate class. +class YuckyBar +{ +public: + YuckyBar(const std::string& which): + mWhich(which) + {} + virtual ~YuckyBar() {} + + virtual std::string identity() const { return std::string("YuckyBar(") + mWhich + ")"; } + +private: + const std::string mWhich; +}; + +// Pretend that this class would be tough to test because, up until we started +// trying to test it, it contained instances of both YuckyFoo and YuckyBar. +// Now we've refactored so it contains LLLazy<YuckyFoo> and LLLazy<YuckyBar>. +// More than that, it contains them by virtue of deriving from +// LLLazyBase<YuckyFoo> and LLLazyBase<YuckyBar>. +// We postulate two different LLLazyBases because, with only one, you need not +// specify *which* get()/set() method you're talking about. That's a simpler +// case. +class NeedsTesting: public LLLazyBase<YuckyFoo>, public LLLazyBase<YuckyBar> +{ +public: + NeedsTesting(): + // mYuckyBar("RealYuckyBar") + LLLazyBase<YuckyBar>(bll::bind(bll::new_ptr<YuckyBar>(), "RealYuckyBar")) + {} + virtual ~NeedsTesting() {} + + virtual std::string describe() const + { + return std::string("NeedsTesting(") + getLazy<YuckyFoo>(this).whoami() + ", " + + getLazy<YuckyBar>(this).identity() + ")"; + } + +private: + // These instance members were moved to LLLazyBases: + // YuckyFoo mYuckyFoo; + // YuckyBar mYuckyBar; +}; + +// Fake up a test YuckyFoo class +class TestFoo: public YuckyFoo +{ +public: + virtual std::string whoami() const { return "TestFoo"; } +}; + +// and a test YuckyBar +class TestBar: public YuckyBar +{ +public: + TestBar(const std::string& which): YuckyBar(which) {} + virtual std::string identity() const + { + return std::string("TestBar(") + YuckyBar::identity() + ")"; + } +}; + +// So here's a test subclass of NeedsTesting that uses TestFoo and TestBar +// instead of YuckyFoo and YuckyBar. +class TestNeedsTesting: public NeedsTesting +{ +public: + TestNeedsTesting() + { + // Exercise setLazy(T*) + setLazy<YuckyFoo>(this, new TestFoo()); + // Exercise setLazy(Factory) + setLazy<YuckyBar>(this, bll::bind(bll::new_ptr<TestBar>(), "TestYuckyBar")); + } + + virtual std::string describe() const + { + return std::string("TestNeedsTesting(") + NeedsTesting::describe() + ")"; + } + + void toolate() + { + setLazy<YuckyFoo>(this, new TestFoo()); + } +}; + +// This class tests having an explicit LLLazy<T> instance as a named member, +// rather than deriving from LLLazyBase<T>. +class LazyMember +{ +public: + YuckyFoo& getYuckyFoo() { return *mYuckyFoo; } + std::string whoisit() const { return mYuckyFoo->whoami(); } + +protected: + LLLazy<YuckyFoo> mYuckyFoo; +}; + +// This is a test subclass of the above, dynamically replacing the +// LLLazy<YuckyFoo> member. +class TestLazyMember: public LazyMember +{ +public: + // use factory setter + TestLazyMember() + { + mYuckyFoo.set(bll::new_ptr<TestFoo>()); + } + + // use instance setter + TestLazyMember(YuckyFoo* instance) + { + mYuckyFoo.set(instance); + } +}; + +/***************************************************************************** +* TUT +*****************************************************************************/ +namespace tut +{ + struct lllazy_data + { + }; + typedef test_group<lllazy_data> lllazy_group; + typedef lllazy_group::object lllazy_object; + lllazy_group lllazygrp("lllazy"); + + template<> template<> + void lllazy_object::test<1>() + { + // Instantiate an official one, just because we can + NeedsTesting nt; + // and a test one + TestNeedsTesting tnt; +// std::cout << nt.describe() << '\n'; + ensure_equals(nt.describe(), "NeedsTesting(YuckyFoo, YuckyBar(RealYuckyBar))"); +// std::cout << tnt.describe() << '\n'; + ensure_equals(tnt.describe(), + "TestNeedsTesting(NeedsTesting(TestFoo, TestBar(YuckyBar(TestYuckyBar))))"); + } + + template<> template<> + void lllazy_object::test<2>() + { + TestNeedsTesting tnt; + std::string threw; + try + { + tnt.toolate(); + } + catch (const LLLazyCommon::InstanceChange& e) + { + threw = e.what(); + } + ensure_contains("InstanceChange exception", threw, "replace LLLazy instance"); + } + + template<> template<> + void lllazy_object::test<3>() + { + { + LazyMember lm; + // operator*() on-demand instantiation + ensure_equals(lm.getYuckyFoo().whoami(), "YuckyFoo"); + } + { + LazyMember lm; + // operator->() on-demand instantiation + ensure_equals(lm.whoisit(), "YuckyFoo"); + } + } + + template<> template<> + void lllazy_object::test<4>() + { + { + // factory setter + TestLazyMember tlm; + ensure_equals(tlm.whoisit(), "TestFoo"); + } + { + // instance setter + TestLazyMember tlm(new TestFoo()); + ensure_equals(tlm.whoisit(), "TestFoo"); + } + } +} // namespace tut diff --git a/indra/llmessage/CMakeLists.txt b/indra/llmessage/CMakeLists.txt index 0f3e159802..c0f7a4d335 100644 --- a/indra/llmessage/CMakeLists.txt +++ b/indra/llmessage/CMakeLists.txt @@ -3,6 +3,7 @@ project(llmessage) include(00-Common) +include(LLAddBuildTest) include(LLCommon) include(LLMath) include(LLMessage) @@ -63,6 +64,7 @@ set(llmessage_SOURCE_FILES llregionpresenceverifier.cpp llsdappservices.cpp llsdhttpserver.cpp + llsdmessage.cpp llsdmessagebuilder.cpp llsdmessagereader.cpp llsdrpcclient.cpp @@ -156,6 +158,7 @@ set(llmessage_HEADER_FILES llregionpresenceverifier.h llsdappservices.h llsdhttpserver.h + llsdmessage.h llsdmessagebuilder.h llsdmessagereader.h llsdrpcclient.h @@ -217,5 +220,6 @@ IF (NOT LINUX AND VIEWER) #ADD_BUILD_TEST(llhttpclientadapter llmessage) ADD_BUILD_TEST(lltrustedmessageservice llmessage) ADD_BUILD_TEST(lltemplatemessagedispatcher llmessage) + # Don't make llmessage depend on llsdmessage_test because ADD_COMM_BUILD_TEST depends on llmessage! + ADD_COMM_BUILD_TEST(llsdmessage "" "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_llsdmessage_peer.py") ENDIF (NOT LINUX AND VIEWER) - diff --git a/indra/llmessage/llsdmessage.cpp b/indra/llmessage/llsdmessage.cpp new file mode 100644 index 0000000000..f663268466 --- /dev/null +++ b/indra/llmessage/llsdmessage.cpp @@ -0,0 +1,150 @@ +/** + * @file llsdmessage.cpp + * @author Nat Goodspeed + * @date 2008-10-31 + * @brief Implementation for llsdmessage. + * + * $LicenseInfo:firstyear=2008&license=viewergpl$ + * Copyright (c) 2008, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if LL_WINDOWS +#pragma warning (disable : 4675) // "resolved by ADL" -- just as I want! +#endif + +// Precompiled header +#include "linden_common.h" +// associated header +#include "llsdmessage.h" +// STL headers +// std headers +// external library headers +// other Linden headers +#include "llevents.h" +#include "llsdserialize.h" +#include "llhttpclient.h" +#include "llmessageconfig.h" +#include "llhost.h" +#include "message.h" +#include "llsdutil.h" + +// Declare a static LLSDMessage instance to ensure that we have a listener as +// soon as someone tries to post on our canonical LLEventPump name. +static LLSDMessage httpListener; + +LLSDMessage::LLSDMessage(): + // Instantiating our own local LLEventPump with a string name the + // constructor is NOT allowed to tweak is a way of ensuring Singleton + // semantics: attempting to instantiate a second LLSDMessage object would + // throw LLEventPump::DupPumpName. + mEventPump("LLHTTPClient") +{ + mEventPump.listen("self", boost::bind(&LLSDMessage::httpListener, this, _1)); +} + +bool LLSDMessage::httpListener(const LLSD& request) +{ + // Extract what we want from the request object. We do it all up front + // partly to document what we expect. + LLSD::String url(request["url"]); + LLSD payload(request["payload"]); + LLSD::String reply(request["reply"]); + LLSD::String error(request["error"]); + LLSD::Real timeout(request["timeout"]); + // If the LLSD doesn't even have a "url" key, we doubt it was intended for + // this listener. + if (url.empty()) + { + std::ostringstream out; + out << "request event without 'url' key to '" << mEventPump.getName() << "'"; + throw ArgError(out.str()); + } + // Establish default timeout. This test relies on LLSD::asReal() returning + // exactly 0.0 for an undef value. + if (! timeout) + { + timeout = HTTP_REQUEST_EXPIRY_SECS; + } + LLHTTPClient::post(url, payload, + new LLSDMessage::EventResponder(LLEventPumps::instance(), + url, "POST", reply, error), + LLSD(), // headers + timeout); + return false; +} + +void LLSDMessage::EventResponder::result(const LLSD& data) +{ + // If our caller passed an empty replyPump name, they're not + // listening: this is a fire-and-forget message. Don't bother posting + // to the pump whose name is "". + if (! mReplyPump.empty()) + { + mPumps.obtain(mReplyPump).post(data); + } + else // default success handling + { + LL_INFOS("LLSDMessage::EventResponder") + << "'" << mMessage << "' to '" << mTarget << "' succeeded" + << LL_ENDL; + } +} + +void LLSDMessage::EventResponder::error(U32 status, const std::string& reason, const LLSD& content) +{ + // If our caller passed an empty errorPump name, they're not + // listening: "default error handling is acceptable." Only post to an + // explicit pump name. + if (! mErrorPump.empty()) + { + LLSD info; + info["target"] = mTarget; + info["message"] = mMessage; + info["status"] = LLSD::Integer(status); + info["reason"] = reason; + info["content"] = content; + mPumps.obtain(mErrorPump).post(info); + } + else // default error handling + { + // convention seems to be to use llinfos, but that seems a bit casual? + LL_WARNS("LLSDMessage::EventResponder") + << "'" << mMessage << "' to '" << mTarget + << "' failed with code " << status << ": " << reason << '\n' + << ll_pretty_print_sd(content) + << LL_ENDL; + } +} + +LLSDMessage::ResponderAdapter::ResponderAdapter(LLHTTPClient::ResponderPtr responder, + const std::string& name): + mResponder(responder), + mReplyPump(name + ".reply", true), // tweak name for uniqueness + mErrorPump(name + ".error", true) +{ + mReplyPump.listen("self", boost::bind(&ResponderAdapter::listener, this, _1, true)); + mErrorPump.listen("self", boost::bind(&ResponderAdapter::listener, this, _1, false)); +} + +bool LLSDMessage::ResponderAdapter::listener(const LLSD& payload, bool success) +{ + if (success) + { + mResponder->result(payload); + } + else + { + mResponder->error(payload["status"].asInteger(), payload["reason"], payload["content"]); + } + + /*---------------- MUST BE LAST STATEMENT BEFORE RETURN ----------------*/ + delete this; + // Destruction of mResponder will usually implicitly free its referent as well + /*------------------------- NOTHING AFTER THIS -------------------------*/ + return false; +} + +void LLSDMessage::link() +{ +} diff --git a/indra/llmessage/llsdmessage.h b/indra/llmessage/llsdmessage.h new file mode 100644 index 0000000000..8ae9451243 --- /dev/null +++ b/indra/llmessage/llsdmessage.h @@ -0,0 +1,146 @@ +/** + * @file llsdmessage.h + * @author Nat Goodspeed + * @date 2008-10-30 + * @brief API intended to unify sending capability, UDP and TCP messages: + * https://wiki.lindenlab.com/wiki/Viewer:Messaging/Messaging_Notes + * + * $LicenseInfo:firstyear=2008&license=viewergpl$ + * Copyright (c) 2008, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_LLSDMESSAGE_H) +#define LL_LLSDMESSAGE_H + +#include "llerror.h" // LOG_CLASS() +#include "llevents.h" // LLEventPumps +#include "llhttpclient.h" +#include <string> +#include <stdexcept> + +class LLSD; + +/** + * Class managing the messaging API described in + * https://wiki.lindenlab.com/wiki/Viewer:Messaging/Messaging_Notes + */ +class LLSDMessage +{ + LOG_CLASS(LLSDMessage); + +public: + LLSDMessage(); + + /// Exception if you specify arguments badly + struct ArgError: public std::runtime_error + { + ArgError(const std::string& what): + std::runtime_error(std::string("ArgError: ") + what) {} + }; + + /** + * The response idiom used by LLSDMessage -- LLEventPump names on which to + * post reply or error -- is designed for the case in which your + * reply/error handlers are methods on the same class as the method + * sending the message. Any state available to the sending method that + * must be visible to the reply/error methods can conveniently be stored + * on that class itself, if it's not already. + * + * The LLHTTPClient::Responder idiom requires a separate instance of a + * separate class so that it can dispatch to the code of interest by + * calling canonical virtual methods. Interesting state must be copied + * into that new object. + * + * With some trepidation, because existing response code is packaged in + * LLHTTPClient::Responder subclasses, we provide this adapter class + * <i>for transitional purposes only.</i> Instantiate a new heap + * ResponderAdapter with your new LLHTTPClient::ResponderPtr. Pass + * ResponderAdapter::getReplyName() and/or getErrorName() in your + * LLSDMessage (or LLViewerRegion::getCapAPI()) request event. The + * ResponderAdapter will call the appropriate Responder method, then + * @c delete itself. + */ + class ResponderAdapter + { + public: + /** + * Bind the new LLHTTPClient::Responder subclass instance. + * + * Passing the constructor a name other than the default is only + * interesting if you suspect some usage will lead to an exception or + * log message. + */ + ResponderAdapter(LLHTTPClient::ResponderPtr responder, + const std::string& name="ResponderAdapter"); + + /// EventPump name on which LLSDMessage should post reply event + std::string getReplyName() const { return mReplyPump.getName(); } + /// EventPump name on which LLSDMessage should post error event + std::string getErrorName() const { return mErrorPump.getName(); } + + private: + // We have two different LLEventStreams, though we route them both to + // the same listener, so that we can bind an extra flag identifying + // which case (reply or error) reached that listener. + bool listener(const LLSD&, bool success); + + LLHTTPClient::ResponderPtr mResponder; + LLEventStream mReplyPump, mErrorPump; + }; + + /** + * Force our implementation file to be linked with caller. The .cpp file + * contains a static instance of this class, which must be linked into the + * executable to support the canonical listener. But since the primary + * interface to that static instance is via a named LLEventPump rather + * than by direct reference, the linker doesn't necessarily perceive the + * necessity to bring in the translation unit. Referencing this dummy + * method forces the issue. + */ + static void link(); + +private: + friend class LLCapabilityListener; + /// Responder used for internal purposes by LLSDMessage and + /// LLCapabilityListener. Others should use higher-level APIs. + class EventResponder: public LLHTTPClient::Responder + { + public: + /** + * LLHTTPClient::Responder that dispatches via named LLEventPump instances. + * We bind LLEventPumps, even though it's an LLSingleton, for testability. + * We bind the string names of the desired LLEventPump instances rather + * than actually obtain()ing them so we only obtain() the one we're going + * to use. If the caller doesn't bother to listen() on it, the other pump + * may never materialize at all. + * @a target and @a message are only to clarify error processing. + * For a capability message, @a target should be the region description, + * @a message should be the capability name. + * For a service with a visible URL, pass the URL as @a target and the HTTP verb + * (e.g. "POST") as @a message. + */ + EventResponder(LLEventPumps& pumps, + const std::string& target, const std::string& message, + const std::string& replyPump, const std::string& errorPump): + mPumps(pumps), + mTarget(target), + mMessage(message), + mReplyPump(replyPump), + mErrorPump(errorPump) + {} + + virtual void result(const LLSD& data); + virtual void error(U32 status, const std::string& reason, const LLSD& content); + + private: + LLEventPumps& mPumps; + const std::string mTarget, mMessage, mReplyPump, mErrorPump; + }; + +private: + bool httpListener(const LLSD&); + LLEventStream mEventPump; +}; + +#endif /* ! defined(LL_LLSDMESSAGE_H) */ diff --git a/indra/llmessage/tests/llsdmessage_test.cpp b/indra/llmessage/tests/llsdmessage_test.cpp new file mode 100644 index 0000000000..2957d7cc4f --- /dev/null +++ b/indra/llmessage/tests/llsdmessage_test.cpp @@ -0,0 +1,113 @@ +/** + * @file llsdmessage_tut.cpp + * @author Nat Goodspeed + * @date 2008-12-22 + * @brief Test of llsdmessage.h + * + * $LicenseInfo:firstyear=2008&license=viewergpl$ + * Copyright (c) 2008, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if LL_WINDOWS +#pragma warning (disable : 4675) // "resolved by ADL" -- just as I want! +#endif + +// Precompiled header +#include "linden_common.h" +// associated header +#include "llsdmessage.h" +// STL headers +#include <iostream> +// std headers +#include <stdexcept> +// external library headers +// other Linden headers +#include "../test/lltut.h" +#include "llsdserialize.h" +#include "llevents.h" +#include "stringize.h" +#include "llhost.h" +#include "tests/networkio.h" +#include "tests/commtest.h" + +/***************************************************************************** +* TUT +*****************************************************************************/ +namespace tut +{ + struct llsdmessage_data: public commtest_data + { + LLEventPump& httpPump; + + llsdmessage_data(): + httpPump(pumps.obtain("LLHTTPClient")) + { + LLSDMessage::link(); + } + }; + typedef test_group<llsdmessage_data> llsdmessage_group; + typedef llsdmessage_group::object llsdmessage_object; + llsdmessage_group llsdmgr("llsdmessage"); + + template<> template<> + void llsdmessage_object::test<1>() + { + bool threw = false; + // This should fail... + try + { + LLSDMessage localListener; + } + catch (const LLEventPump::DupPumpName&) + { + threw = true; + } + ensure("second LLSDMessage should throw", threw); + } + + template<> template<> + void llsdmessage_object::test<2>() + { + LLSD request, body; + body["data"] = "yes"; + request["payload"] = body; + request["reply"] = replyPump.getName(); + request["error"] = errorPump.getName(); + bool threw = false; + try + { + httpPump.post(request); + } + catch (const LLSDMessage::ArgError&) + { + threw = true; + } + ensure("missing URL", threw); + } + + template<> template<> + void llsdmessage_object::test<3>() + { + LLSD request, body; + body["data"] = "yes"; + request["url"] = server + "got-message"; + request["payload"] = body; + request["reply"] = replyPump.getName(); + request["error"] = errorPump.getName(); + httpPump.post(request); + ensure("got response", netio.pump()); + ensure("success response", success); + ensure_equals(result.asString(), "success"); + + body["status"] = 499; + body["reason"] = "custom error message"; + request["url"] = server + "fail"; + request["payload"] = body; + httpPump.post(request); + ensure("got response", netio.pump()); + ensure("failure response", ! success); + ensure_equals(result["status"].asInteger(), body["status"].asInteger()); + ensure_equals(result["reason"].asString(), body["reason"].asString()); + } +} // namespace tut diff --git a/indra/llui/llmenugl.cpp b/indra/llui/llmenugl.cpp index 0762d7c12d..a8d06643ff 100644 --- a/indra/llui/llmenugl.cpp +++ b/indra/llui/llmenugl.cpp @@ -67,6 +67,8 @@ #include <set> #include <boost/tokenizer.hpp> +using namespace LLOldEvents; + // static LLMenuHolderGL *LLMenuGL::sMenuContainer = NULL; diff --git a/indra/llui/llmenugl.h b/indra/llui/llmenugl.h index 030b81b58c..e62402d617 100644 --- a/indra/llui/llmenugl.h +++ b/indra/llui/llmenugl.h @@ -224,7 +224,7 @@ private: // calls a user defined callback. //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -class LLMenuItemCallGL : public LLMenuItemGL, public LLObservable +class LLMenuItemCallGL : public LLMenuItemGL, public LLOldEvents::LLObservable { public: // normal constructor diff --git a/indra/llui/llnotifications.cpp b/indra/llui/llnotifications.cpp index 6b7dd0a3de..fe1ea95070 100644 --- a/indra/llui/llnotifications.cpp +++ b/indra/llui/llnotifications.cpp @@ -714,7 +714,7 @@ std::string LLNotification::getLabel() const // ========================================================= // LLNotificationChannel implementation // --- -void LLNotificationChannelBase::connectChanged(const LLStandardSignal::slot_type& slot) +LLBoundListener LLNotificationChannelBase::connectChangedImpl(const LLEventListener& slot) { // when someone wants to connect to a channel, we first throw them // all of the notifications that are already in the channel @@ -722,23 +722,23 @@ void LLNotificationChannelBase::connectChanged(const LLStandardSignal::slot_type // only about new notifications for (LLNotificationSet::iterator it = mItems.begin(); it != mItems.end(); ++it) { - slot.get_slot_function()(LLSD().insert("sigtype", "load").insert("id", (*it)->id())); + slot(LLSD().insert("sigtype", "load").insert("id", (*it)->id())); } // and then connect the signal so that all future notifications will also be // forwarded. - mChanged.connect(slot); + return mChanged.connect(slot); } -void LLNotificationChannelBase::connectPassedFilter(const LLStandardSignal::slot_type& slot) +LLBoundListener LLNotificationChannelBase::connectPassedFilterImpl(const LLEventListener& slot) { // these two filters only fire for notifications added after the current one, because // they don't participate in the hierarchy. - mPassedFilter.connect(slot); + return mPassedFilter.connect(slot); } -void LLNotificationChannelBase::connectFailedFilter(const LLStandardSignal::slot_type& slot) +LLBoundListener LLNotificationChannelBase::connectFailedFilterImpl(const LLEventListener& slot) { - mFailedFilter.connect(slot); + return mFailedFilter.connect(slot); } // external call, conforms to our standard signature @@ -896,8 +896,7 @@ mParent(parent) else { LLNotificationChannelPtr p = LLNotifications::instance().getChannel(parent); - LLStandardSignal::slot_type f = boost::bind(&LLNotificationChannelBase::updateItem, this, _1); - p->connectChanged(f); + p->connectChanged(boost::bind(&LLNotificationChannelBase::updateItem, this, _1)); } } @@ -1093,11 +1092,11 @@ void LLNotifications::createDefaultChannels() // connect action methods to these channels LLNotifications::instance().getChannel("Expiration")-> - connectChanged(boost::bind(&LLNotifications::expirationHandler, this, _1)); + connectChanged(boost::bind(&LLNotifications::expirationHandler, this, _1)); LLNotifications::instance().getChannel("Unique")-> - connectChanged(boost::bind(&LLNotifications::uniqueHandler, this, _1)); + connectChanged(boost::bind(&LLNotifications::uniqueHandler, this, _1)); LLNotifications::instance().getChannel("Unique")-> - connectFailedFilter(boost::bind(&LLNotifications::failedUniquenessTest, this, _1)); + connectFailedFilter(boost::bind(&LLNotifications::failedUniquenessTest, this, _1)); LLNotifications::instance().getChannel("Ignore")-> connectFailedFilter(&handleIgnoredNotification); } diff --git a/indra/llui/llnotifications.h b/indra/llui/llnotifications.h index bb379121cc..d01296c89e 100644 --- a/indra/llui/llnotifications.h +++ b/indra/llui/llnotifications.h @@ -91,49 +91,20 @@ #include <boost/utility.hpp> #include <boost/shared_ptr.hpp> -#include <boost/signal.hpp> +#include <boost/enable_shared_from_this.hpp> #include <boost/type_traits.hpp> // we want to minimize external dependencies, but this one is important #include "llsd.h" // and we need this to manage the notification callbacks +#include "llevents.h" #include "llfunctorregistry.h" #include "llui.h" class LLNotification; typedef boost::shared_ptr<LLNotification> LLNotificationPtr; -/***************************************************************************** -* Signal and handler declarations -* Using a single handler signature means that we can have a common handler -* type, rather than needing a distinct one for each different handler. -*****************************************************************************/ - -/** - * A boost::signals Combiner that stops the first time a handler returns true - * We need this because we want to have our handlers return bool, so that - * we have the option to cause a handler to stop further processing. The - * default handler fails when the signal returns a value but has no slots. - */ -struct LLStopWhenHandled -{ - typedef bool result_type; - - template<typename InputIterator> - result_type operator()(InputIterator first, InputIterator last) const - { - for (InputIterator si = first; si != last; ++si) - { - if (*si) - { - return true; - } - } - return false; - } -}; - typedef enum e_notification_priority { @@ -144,27 +115,11 @@ typedef enum e_notification_priority NOTIFICATION_PRIORITY_CRITICAL } ENotificationPriority; -/** - * We want to have a standard signature for all signals; this way, - * we can easily document a protocol for communicating across - * dlls and into scripting languages someday. - * we want to return a bool to indicate whether the signal has been - * handled and should NOT be passed on to other listeners. - * Return true to stop further handling of the signal, and false - * to continue. - * We take an LLSD because this way the contents of the signal - * are independent of the API used to communicate it. - * It is const ref because then there's low cost to pass it; - * if you only need to inspect it, it's very cheap. - */ - typedef boost::function<void (const LLSD&, const LLSD&)> LLNotificationResponder; typedef LLFunctorRegistry<LLNotificationResponder> LLNotificationFunctorRegistry; typedef LLFunctorRegistration<LLNotificationResponder> LLNotificationFunctorRegistration; -typedef boost::signal<bool(const LLSD&), LLStopWhenHandled> LLStandardSignal; - // context data that can be looked up via a notification's payload by the display logic // derive from this class to implement specific contexts class LLNotificationContext : public LLInstanceTracker<LLNotificationContext, LLUUID> @@ -699,7 +654,7 @@ typedef std::multimap<std::string, LLNotificationPtr> LLNotificationMap; // all of the built-in tests should attach to the "Visible" channel // class LLNotificationChannelBase : - public boost::signals::trackable + public LLEventTrackable { LOG_CLASS(LLNotificationChannelBase); public: @@ -709,15 +664,45 @@ public: virtual ~LLNotificationChannelBase() {} // you can also connect to a Channel, so you can be notified of // changes to this channel - virtual void connectChanged(const LLStandardSignal::slot_type& slot); - virtual void connectPassedFilter(const LLStandardSignal::slot_type& slot); - virtual void connectFailedFilter(const LLStandardSignal::slot_type& slot); + template <typename LISTENER> + LLBoundListener connectChanged(const LISTENER& slot) + { + // Examine slot to see if it binds an LLEventTrackable subclass, or a + // boost::shared_ptr to something, or a boost::weak_ptr to something. + // Call this->connectChangedImpl() to actually connect it. + return LLEventDetail::visit_and_connect(slot, + boost::bind(&LLNotificationChannelBase::connectChangedImpl, + this, + _1)); + } + template <typename LISTENER> + LLBoundListener connectPassedFilter(const LISTENER& slot) + { + // see comments in connectChanged() + return LLEventDetail::visit_and_connect(slot, + boost::bind(&LLNotificationChannelBase::connectPassedFilterImpl, + this, + _1)); + } + template <typename LISTENER> + LLBoundListener connectFailedFilter(const LISTENER& slot) + { + // see comments in connectChanged() + return LLEventDetail::visit_and_connect(slot, + boost::bind(&LLNotificationChannelBase::connectFailedFilterImpl, + this, + _1)); + } // use this when items change or to add a new one bool updateItem(const LLSD& payload); const LLNotificationFilter& getFilter() { return mFilter; } protected: + LLBoundListener connectChangedImpl(const LLEventListener& slot); + LLBoundListener connectPassedFilterImpl(const LLEventListener& slot); + LLBoundListener connectFailedFilterImpl(const LLEventListener& slot); + LLNotificationSet mItems; LLStandardSignal mChanged; LLStandardSignal mPassedFilter; diff --git a/indra/llui/llpanel.h b/indra/llui/llpanel.h index 756d02ef7d..a7c9579030 100644 --- a/indra/llui/llpanel.h +++ b/indra/llui/llpanel.h @@ -42,6 +42,7 @@ #include "llviewborder.h" #include "lluistring.h" #include "v4color.h" +#include "llevents.h" #include <list> #include <queue> @@ -56,7 +57,7 @@ const BOOL BORDER_NO = FALSE; * With or without border, * Can contain LLUICtrls. */ -class LLPanel : public LLUICtrl, public boost::signals::trackable +class LLPanel : public LLUICtrl, public LLEventTrackable { public: diff --git a/indra/llui/llui.h b/indra/llui/llui.h index ebcc7304b1..6d6ce7a97c 100644 --- a/indra/llui/llui.h +++ b/indra/llui/llui.h @@ -43,7 +43,7 @@ #include "llgl.h" // *TODO: break this dependency #include <stack> //#include "llimagegl.h" -#include <boost/signal.hpp> +#include <boost/signals2.hpp> // LLUIFactory #include "llsd.h" @@ -661,7 +661,7 @@ template <typename T> std::set<T*> LLInstanceTracker<T, T*>::sInstances; class LLCallbackRegistry { public: - typedef boost::signal<void()> callback_signal_t; + typedef boost::signals2::signal<void()> callback_signal_t; void registerCallback(const callback_signal_t::slot_type& slot) { diff --git a/indra/llui/llview.cpp b/indra/llui/llview.cpp index 2350ea6050..8ec681fcaf 100644 --- a/indra/llui/llview.cpp +++ b/indra/llui/llview.cpp @@ -56,6 +56,8 @@ #include "lltexteditor.h" #include "lltextbox.h" +using namespace LLOldEvents; + //HACK: this allows you to instantiate LLView from xml with "<view/>" which we don't want static LLRegisterWidget<LLView> r("view"); diff --git a/indra/llui/llview.h b/indra/llui/llview.h index e0e0f6ba47..721fe99f4a 100644 --- a/indra/llui/llview.h +++ b/indra/llui/llview.h @@ -441,11 +441,11 @@ public: void localRectToScreen( const LLRect& local, LLRect* screen ) const; // Listener dispatching functions (Dispatcher deletes pointers to listeners on deregistration or destruction) - LLSimpleListener* getListenerByName(const std::string& callback_name); - void registerEventListener(std::string name, LLSimpleListener* function); + LLOldEvents::LLSimpleListener* getListenerByName(const std::string& callback_name); + void registerEventListener(std::string name, LLOldEvents::LLSimpleListener* function); void deregisterEventListener(std::string name); - std::string findEventListener(LLSimpleListener *listener) const; - void addListenerToControl(LLEventDispatcher *observer, const std::string& name, LLSD filter, LLSD userdata); + std::string findEventListener(LLOldEvents::LLSimpleListener *listener) const; + void addListenerToControl(LLOldEvents::LLEventDispatcher *observer, const std::string& name, LLSD filter, LLSD userdata); void addBoolControl(const std::string& name, bool initial_value); LLControlVariable *getControl(const std::string& name); @@ -651,7 +651,7 @@ private: static LLWindow* sWindow; // All root views must know about their window. - typedef std::map<std::string, LLPointer<LLSimpleListener> > dispatch_list_t; + typedef std::map<std::string, LLPointer<LLOldEvents::LLSimpleListener> > dispatch_list_t; dispatch_list_t mDispatchList; std::string mControlName; @@ -659,7 +659,7 @@ private: typedef std::map<std::string, LLView*> dummy_widget_map_t; mutable dummy_widget_map_t mDummyWidgets; - boost::signals::connection mControlConnection; + boost::signals2::connection mControlConnection; ECursorType mHoverCursor; diff --git a/indra/llxml/CMakeLists.txt b/indra/llxml/CMakeLists.txt index dc7787beea..c5fb44e721 100644 --- a/indra/llxml/CMakeLists.txt +++ b/indra/llxml/CMakeLists.txt @@ -39,6 +39,5 @@ add_library (llxml ${llxml_SOURCE_FILES}) # Sort by high-level to low-level target_link_libraries( llxml llmath - ${BOOST_SIGNALS_LIBRARY} ${EXPAT_LIBRARIES} ) diff --git a/indra/llxml/llcontrol.h b/indra/llxml/llcontrol.h index ba0a1c7cbf..a39a56aacd 100644 --- a/indra/llxml/llcontrol.h +++ b/indra/llxml/llcontrol.h @@ -53,7 +53,7 @@ #endif #include <boost/bind.hpp> -#include <boost/signal.hpp> +#include <boost/signals2.hpp> #if LL_WINDOWS # if (_MSC_VER >= 1300 && _MSC_VER < 1400) @@ -89,7 +89,7 @@ typedef enum e_control_type class LLControlVariable : public LLRefCount { friend class LLControlGroup; - typedef boost::signal<void(const LLSD&)> signal_t; + typedef boost::signals2::signal<void(const LLSD&)> signal_t; private: std::string mName; diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 7b1c3003f0..57f1716db5 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -78,6 +78,7 @@ set(viewer_SOURCE_FILES llbox.cpp llcallbacklist.cpp llcallingcard.cpp + llcapabilitylistener.cpp llcaphttpsender.cpp llchatbar.cpp llclassifiedinfo.cpp @@ -481,6 +482,8 @@ set(viewer_HEADER_FILES llbox.h llcallbacklist.h llcallingcard.h + llcapabilitylistener.h + llcapabilityprovider.h llcaphttpsender.h llchatbar.h llclassifiedinfo.h @@ -1391,3 +1394,5 @@ if (INSTALL) endif (INSTALL) ADD_VIEWER_BUILD_TEST(llagentaccess viewer) +ADD_VIEWER_COMM_BUILD_TEST(llcapabilitylistener viewer + ${CMAKE_CURRENT_SOURCE_DIR}/../llmessage/tests/test_llsdmessage_peer.py) diff --git a/indra/newview/llagent.cpp b/indra/newview/llagent.cpp index 98526c8a57..07503404dc 100644 --- a/indra/newview/llagent.cpp +++ b/indra/newview/llagent.cpp @@ -134,6 +134,8 @@ #include "llappviewer.h" #include "llviewerjoystick.h" #include "llfollowcam.h" +#include "stringize.h" +#include "llcapabilitylistener.h" using namespace LLVOAvatarDefines; @@ -914,7 +916,7 @@ LLViewerRegion *LLAgent::getRegion() const } -const LLHost& LLAgent::getRegionHost() const +LLHost LLAgent::getRegionHost() const { if (mRegionp) { @@ -4674,109 +4676,128 @@ void LLAgent::lookAtLastChat() const F32 SIT_POINT_EXTENTS = 0.2f; +LLSD ll_sdmap_from_vector3(const LLVector3& vec) +{ + LLSD ret; + ret["X"] = vec.mV[VX]; + ret["Y"] = vec.mV[VY]; + ret["Z"] = vec.mV[VZ]; + return ret; +} + +LLVector3 ll_vector3_from_sdmap(const LLSD& sd) +{ + LLVector3 ret; + ret.mV[VX] = F32(sd["X"].asReal()); + ret.mV[VY] = F32(sd["Y"].asReal()); + ret.mV[VZ] = F32(sd["Z"].asReal()); + return ret; +} + void LLAgent::setStartPosition( U32 location_id ) { - LLViewerObject *object; + LLViewerObject *object; - if ( !(gAgentID == LLUUID::null) ) - { + if (gAgentID == LLUUID::null) + { + return; + } // we've got an ID for an agent viewerobject object = gObjectList.findObject(gAgentID); - if (object) + if (! object) { - // we've got the viewer object - // Sometimes the agent can be velocity interpolated off of - // this simulator. Clamp it to the region the agent is - // in, a little bit in on each side. - const F32 INSET = 0.5f; //meters - const F32 REGION_WIDTH = LLWorld::getInstance()->getRegionWidthInMeters(); - - LLVector3 agent_pos = getPositionAgent(); - LLVector3 agent_look_at = mFrameAgent.getAtAxis(); - - if (mAvatarObject.notNull()) - { - // the z height is at the agent's feet - agent_pos.mV[VZ] -= 0.5f * mAvatarObject->mBodySize.mV[VZ]; - } - - agent_pos.mV[VX] = llclamp( agent_pos.mV[VX], INSET, REGION_WIDTH - INSET ); - agent_pos.mV[VY] = llclamp( agent_pos.mV[VY], INSET, REGION_WIDTH - INSET ); - - // Don't let them go below ground, or too high. - agent_pos.mV[VZ] = llclamp( agent_pos.mV[VZ], - mRegionp->getLandHeightRegion( agent_pos ), - LLWorld::getInstance()->getRegionMaxHeight() ); - // Send the CapReq - - LLSD body; - - std::string url = gAgent.getRegion()->getCapability("HomeLocation"); - std::ostringstream strBuffer; - if( url.empty() ) - { - LLMessageSystem* msg = gMessageSystem; - msg->newMessageFast(_PREHASH_SetStartLocationRequest); - msg->nextBlockFast( _PREHASH_AgentData); - msg->addUUIDFast(_PREHASH_AgentID, getID()); - msg->addUUIDFast(_PREHASH_SessionID, getSessionID()); - msg->nextBlockFast( _PREHASH_StartLocationData); - // corrected by sim - msg->addStringFast(_PREHASH_SimName, ""); - msg->addU32Fast(_PREHASH_LocationID, location_id); - msg->addVector3Fast(_PREHASH_LocationPos, agent_pos); - msg->addVector3Fast(_PREHASH_LocationLookAt,mFrameAgent.getAtAxis()); - - // Reliable only helps when setting home location. Last - // location is sent on quit, and we don't have time to ack - // the packets. - msg->sendReliable(mRegionp->getHost()); - - const U32 HOME_INDEX = 1; - if( HOME_INDEX == location_id ) - { - setHomePosRegion( mRegionp->getHandle(), getPositionAgent() ); - } - } - else - { - strBuffer << location_id; - body["HomeLocation"]["LocationId"] = strBuffer.str(); - - strBuffer.str(""); - strBuffer << agent_pos.mV[VX]; - body["HomeLocation"]["LocationPos"]["X"] = strBuffer.str(); - - strBuffer.str(""); - strBuffer << agent_pos.mV[VY]; - body["HomeLocation"]["LocationPos"]["Y"] = strBuffer.str(); - - strBuffer.str(""); - strBuffer << agent_pos.mV[VZ]; - body["HomeLocation"]["LocationPos"]["Z"] = strBuffer.str(); - - strBuffer.str(""); - strBuffer << agent_look_at.mV[VX]; - body["HomeLocation"]["LocationLookAt"]["X"] = strBuffer.str(); - - strBuffer.str(""); - strBuffer << agent_look_at.mV[VY]; - body["HomeLocation"]["LocationLookAt"]["Y"] = strBuffer.str(); - - strBuffer.str(""); - strBuffer << agent_look_at.mV[VZ]; - body["HomeLocation"]["LocationLookAt"]["Z"] = strBuffer.str(); - - LLHTTPClient::post( url, body, new LLHomeLocationResponder() ); - } + llinfos << "setStartPosition - Can't find agent viewerobject id " << gAgentID << llendl; + return; + } + // we've got the viewer object + // Sometimes the agent can be velocity interpolated off of + // this simulator. Clamp it to the region the agent is + // in, a little bit in on each side. + const F32 INSET = 0.5f; //meters + const F32 REGION_WIDTH = LLWorld::getInstance()->getRegionWidthInMeters(); + + LLVector3 agent_pos = getPositionAgent(); + + if (mAvatarObject.notNull()) + { + // the z height is at the agent's feet + agent_pos.mV[VZ] -= 0.5f * mAvatarObject->mBodySize.mV[VZ]; } - else + + agent_pos.mV[VX] = llclamp( agent_pos.mV[VX], INSET, REGION_WIDTH - INSET ); + agent_pos.mV[VY] = llclamp( agent_pos.mV[VY], INSET, REGION_WIDTH - INSET ); + + // Don't let them go below ground, or too high. + agent_pos.mV[VZ] = llclamp( agent_pos.mV[VZ], + mRegionp->getLandHeightRegion( agent_pos ), + LLWorld::getInstance()->getRegionMaxHeight() ); + // Send the CapReq + LLSD request; + LLSD body; + LLSD homeLocation; + + homeLocation["LocationId"] = LLSD::Integer(location_id); + homeLocation["LocationPos"] = ll_sdmap_from_vector3(agent_pos); + homeLocation["LocationLookAt"] = ll_sdmap_from_vector3(mFrameAgent.getAtAxis()); + + body["HomeLocation"] = homeLocation; + + // This awkward idiom warrants explanation. + // For starters, LLSDMessage::ResponderAdapter is ONLY for testing the new + // LLSDMessage functionality with a pre-existing LLHTTPClient::Responder. + // In new code, define your reply/error methods on the same class as the + // sending method, bind them to local LLEventPump objects and pass those + // LLEventPump names in the request LLSD object. + // When testing old code, the new LLHomeLocationResponder object + // is referenced by an LLHTTPClient::ResponderPtr, so when the + // ResponderAdapter is deleted, the LLHomeLocationResponder will be too. + // We must trust that the underlying LLHTTPClient code will eventually + // fire either the reply callback or the error callback; either will cause + // the ResponderAdapter to delete itself. + LLSDMessage::ResponderAdapter* + adapter(new LLSDMessage::ResponderAdapter(new LLHomeLocationResponder())); + + request["message"] = "HomeLocation"; + request["payload"] = body; + request["reply"] = adapter->getReplyName(); + request["error"] = adapter->getErrorName(); + + gAgent.getRegion()->getCapAPI().post(request); + + const U32 HOME_INDEX = 1; + if( HOME_INDEX == location_id ) { - llinfos << "setStartPosition - Can't find agent viewerobject id " << gAgentID << llendl; + setHomePosRegion( mRegionp->getHandle(), getPositionAgent() ); } - } } +struct HomeLocationMapper: public LLCapabilityListener::CapabilityMapper +{ + // No reply message expected + HomeLocationMapper(): LLCapabilityListener::CapabilityMapper("HomeLocation") {} + virtual void buildMessage(LLMessageSystem* msg, + const LLUUID& agentID, + const LLUUID& sessionID, + const std::string& capabilityName, + const LLSD& payload) const + { + msg->newMessageFast(_PREHASH_SetStartLocationRequest); + msg->nextBlockFast( _PREHASH_AgentData); + msg->addUUIDFast(_PREHASH_AgentID, agentID); + msg->addUUIDFast(_PREHASH_SessionID, sessionID); + msg->nextBlockFast( _PREHASH_StartLocationData); + // corrected by sim + msg->addStringFast(_PREHASH_SimName, ""); + msg->addU32Fast(_PREHASH_LocationID, payload["HomeLocation"]["LocationId"].asInteger()); + msg->addVector3Fast(_PREHASH_LocationPos, + ll_vector3_from_sdmap(payload["HomeLocation"]["LocationPos"])); + msg->addVector3Fast(_PREHASH_LocationLookAt, + ll_vector3_from_sdmap(payload["HomeLocation"]["LocationLookAt"])); + } +}; +// Need an instance of this class so it will self-register +static HomeLocationMapper homeLocationMapper; + void LLAgent::requestStopMotion( LLMotion* motion ) { // Notify all avatars that a motion has stopped. @@ -5400,7 +5421,7 @@ void update_group_floaters(const LLUUID& group_id) gIMMgr->refresh(); } - gAgent.fireEvent(new LLEvent(&gAgent, "new group"), ""); + gAgent.fireEvent(new LLOldEvents::LLEvent(&gAgent, "new group"), ""); } // static diff --git a/indra/newview/llagent.h b/indra/newview/llagent.h index 226c78e631..27fcb07fd5 100644 --- a/indra/newview/llagent.h +++ b/indra/newview/llagent.h @@ -119,7 +119,7 @@ inline bool operator==(const LLGroupData &a, const LLGroupData &b) // -class LLAgent : public LLObservable +class LLAgent : public LLOldEvents::LLObservable { LOG_CLASS(LLAgent); @@ -176,7 +176,7 @@ public: // Set the home data void setRegion(LLViewerRegion *regionp); LLViewerRegion *getRegion() const; - const LLHost& getRegionHost() const; + LLHost getRegionHost() const; std::string getSLURL() const; void updateAgentPosition(const F32 dt, const F32 yaw, const S32 mouse_x, const S32 mouse_y); // call once per frame to update position, angles radians diff --git a/indra/newview/llagentlanguage.h b/indra/newview/llagentlanguage.h index 6bc7250c6e..596c584232 100644 --- a/indra/newview/llagentlanguage.h +++ b/indra/newview/llagentlanguage.h @@ -36,7 +36,7 @@ #include "llmemory.h" // LLSingleton<> #include "llevent.h" -class LLAgentLanguage: public LLSingleton<LLAgentLanguage>, public LLSimpleListener +class LLAgentLanguage: public LLSingleton<LLAgentLanguage>, public LLOldEvents::LLSimpleListener { public: LLAgentLanguage(); diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 0c34d018a2..b66b514597 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -91,6 +91,7 @@ #include "lltexturecache.h" #include "lltexturefetch.h" #include "llimageworker.h" +#include "llevents.h" // The files below handle dependencies from cleanup. #include "llkeyframemotion.h" @@ -841,7 +842,14 @@ bool LLAppViewer::mainLoop() LLTimer debugTime; LLViewerJoystick* joystick(LLViewerJoystick::getInstance()); joystick->setNeedsReset(true); - + + LLEventPump& mainloop(LLEventPumps::instance().obtain("mainloop")); + // As we do not (yet) send data on the mainloop LLEventPump that varies + // with each frame, no need to instantiate a new LLSD event object each + // time. Obviously, if that changes, just instantiate the LLSD at the + // point of posting. + LLSD newFrame; + // Handle messages while (!LLApp::isExiting()) { @@ -882,6 +890,9 @@ bool LLAppViewer::mainLoop() LLFloaterMemLeak::getInstance()->idle() ; } + // canonical per-frame event + mainloop.post(newFrame); + if (!LLApp::isExiting()) { pingMainloopTimeout("Main:JoystickKeyboard"); diff --git a/indra/newview/llcapabilitylistener.cpp b/indra/newview/llcapabilitylistener.cpp new file mode 100644 index 0000000000..3277da8930 --- /dev/null +++ b/indra/newview/llcapabilitylistener.cpp @@ -0,0 +1,183 @@ +/** + * @file llcapabilitylistener.cpp + * @author Nat Goodspeed + * @date 2009-01-07 + * @brief Implementation for llcapabilitylistener. + * + * $LicenseInfo:firstyear=2009&license=viewergpl$ + * Copyright (c) 2009, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "llviewerprecompiledheaders.h" +// associated header +#include "llcapabilitylistener.h" +// STL headers +#include <map> +// std headers +// external library headers +#include <boost/bind.hpp> +// other Linden headers +#include "stringize.h" +#include "llcapabilityprovider.h" + +class LLCapabilityListener::CapabilityMappers: public LLSingleton<LLCapabilityListener::CapabilityMappers> +{ +public: + void registerMapper(const LLCapabilityListener::CapabilityMapper*); + void unregisterMapper(const LLCapabilityListener::CapabilityMapper*); + const LLCapabilityListener::CapabilityMapper* find(const std::string& cap) const; + + struct DupCapMapper: public std::runtime_error + { + DupCapMapper(const std::string& what): + std::runtime_error(std::string("DupCapMapper: ") + what) + {} + }; + +private: + friend class LLSingleton<LLCapabilityListener::CapabilityMappers>; + CapabilityMappers(); + + typedef std::map<std::string, const LLCapabilityListener::CapabilityMapper*> CapabilityMap; + CapabilityMap mMap; +}; + +LLCapabilityListener::LLCapabilityListener(const std::string& name, + LLMessageSystem* messageSystem, + const LLCapabilityProvider& provider, + const LLUUID& agentID, + const LLUUID& sessionID): + mEventPump(name), + mMessageSystem(messageSystem), + mProvider(provider), + mAgentID(agentID), + mSessionID(sessionID) +{ + mEventPump.listen("self", boost::bind(&LLCapabilityListener::capListener, this, _1)); +} + +bool LLCapabilityListener::capListener(const LLSD& request) +{ + // Extract what we want from the request object. We do it all up front + // partly to document what we expect. + LLSD::String cap(request["message"]); + LLSD payload(request["payload"]); + LLSD::String reply(request["reply"]); + LLSD::String error(request["error"]); + LLSD::Real timeout(request["timeout"]); + // If the LLSD doesn't even have a "message" key, we doubt it was intended + // for this listener. + if (cap.empty()) + { + LL_ERRS("capListener") << "capability request event without 'message' key to '" + << getCapAPI().getName() + << "' on region\n" << mProvider.getDescription() + << LL_ENDL; + return false; // in case fatal-error function isn't + } + // Establish default timeout. This test relies on LLSD::asReal() returning + // exactly 0.0 for an undef value. + if (! timeout) + { + timeout = HTTP_REQUEST_EXPIRY_SECS; + } + // Look up the url for the requested capability name. + std::string url = mProvider.getCapability(cap); + if (! url.empty()) + { + // This capability is supported by the region to which we're talking. + LLHTTPClient::post(url, payload, + new LLSDMessage::EventResponder(LLEventPumps::instance(), + mProvider.getDescription(), + cap, reply, error), + LLSD(), // headers + timeout); + } + else + { + // Capability not supported -- do we have a registered mapper? + const CapabilityMapper* mapper = CapabilityMappers::instance().find(cap); + if (! mapper) // capability neither supported nor mapped + { + LL_ERRS("capListener") << "unsupported capability '" << cap << "' request to '" + << getCapAPI().getName() << "' on region\n" + << mProvider.getDescription() + << LL_ENDL; + } + else if (! mapper->getReplyName().empty()) // mapper expects reply support + { + LL_ERRS("capListener") << "Mapper for capability '" << cap + << "' requires unimplemented support for reply message '" + << mapper->getReplyName() + << "' on '" << getCapAPI().getName() << "' on region\n" + << mProvider.getDescription() + << LL_ENDL; + } + else + { + LL_INFOS("capListener") << "fallback invoked for capability '" << cap + << "' request to '" << getCapAPI().getName() + << "' on region\n" << mProvider.getDescription() + << LL_ENDL; + mapper->buildMessage(mMessageSystem, mAgentID, mSessionID, cap, payload); + mMessageSystem->sendReliable(mProvider.getHost()); + } + } + return false; +} + +LLCapabilityListener::CapabilityMapper::CapabilityMapper(const std::string& cap, const std::string& reply): + mCapName(cap), + mReplyName(reply) +{ + LLCapabilityListener::CapabilityMappers::instance().registerMapper(this); +} + +LLCapabilityListener::CapabilityMapper::~CapabilityMapper() +{ + LLCapabilityListener::CapabilityMappers::instance().unregisterMapper(this); +} + +LLSD LLCapabilityListener::CapabilityMapper::readResponse(LLMessageSystem* messageSystem) const +{ + return LLSD(); +} + +LLCapabilityListener::CapabilityMappers::CapabilityMappers() {} + +void LLCapabilityListener::CapabilityMappers::registerMapper(const LLCapabilityListener::CapabilityMapper* mapper) +{ + // Try to insert a new map entry by which we can look up the passed mapper + // instance. + std::pair<CapabilityMap::iterator, bool> inserted = + mMap.insert(CapabilityMap::value_type(mapper->getCapName(), mapper)); + // If we already have a mapper for that name, insert() merely located the + // existing iterator and returned false. It is a coding error to try to + // register more than one mapper for the same capability name. + if (! inserted.second) + { + throw DupCapMapper(std::string("Duplicate capability name ") + mapper->getCapName()); + } +} + +void LLCapabilityListener::CapabilityMappers::unregisterMapper(const LLCapabilityListener::CapabilityMapper* mapper) +{ + CapabilityMap::iterator found = mMap.find(mapper->getCapName()); + if (found != mMap.end()) + { + mMap.erase(found); + } +} + +const LLCapabilityListener::CapabilityMapper* +LLCapabilityListener::CapabilityMappers::find(const std::string& cap) const +{ + CapabilityMap::const_iterator found = mMap.find(cap); + if (found != mMap.end()) + { + return found->second; + } + return NULL; +} diff --git a/indra/newview/llcapabilitylistener.h b/indra/newview/llcapabilitylistener.h new file mode 100644 index 0000000000..061227e04d --- /dev/null +++ b/indra/newview/llcapabilitylistener.h @@ -0,0 +1,113 @@ +/** + * @file llcapabilitylistener.h + * @author Nat Goodspeed + * @date 2009-01-07 + * @brief Provide an event-based API for capability requests + * + * $LicenseInfo:firstyear=2009&license=viewergpl$ + * Copyright (c) 2009, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_LLCAPABILITYLISTENER_H) +#define LL_LLCAPABILITYLISTENER_H + +#include "llevents.h" // LLEventPump +#include "llsdmessage.h" // LLSDMessage::ArgError +#include "llerror.h" // LOG_CLASS() + +class LLCapabilityProvider; +class LLSD; + +class LLCapabilityListener +{ + LOG_CLASS(LLCapabilityListener); +public: + LLCapabilityListener(const std::string& name, LLMessageSystem* messageSystem, + const LLCapabilityProvider& provider, + const LLUUID& agentID, const LLUUID& sessionID); + + /// Capability-request exception + typedef LLSDMessage::ArgError ArgError; + /// Get LLEventPump on which we listen for capability requests + /// (https://wiki.lindenlab.com/wiki/Viewer:Messaging/Messaging_Notes#Capabilities) + LLEventPump& getCapAPI() { return mEventPump; } + + /** + * Base class for mapping an as-yet-undeployed capability name to a (pair + * of) LLMessageSystem message(s). To map a capability name to such + * messages, derive a subclass of CapabilityMapper and declare a static + * instance in a translation unit known to be loaded. The mapping is not + * region-specific. If an LLViewerRegion's capListener() receives a + * request for a supported capability, it will use the capability's URL. + * If not, it will look for an applicable CapabilityMapper subclass + * instance. + */ + class CapabilityMapper + { + public: + /** + * Base-class constructor. Typically your subclass constructor will + * pass these parameters as literals. + * @param cap the capability name handled by this (subclass) instance + * @param reply the name of the response LLMessageSystem message. Omit + * if the LLMessageSystem message you intend to send doesn't prompt a + * reply message, or if you already handle that message in some other + * way. + */ + CapabilityMapper(const std::string& cap, const std::string& reply = ""); + virtual ~CapabilityMapper(); + /// query the capability name + std::string getCapName() const { return mCapName; } + /// query the reply message name + std::string getReplyName() const { return mReplyName; } + /** + * Override this method to build the LLMessageSystem message we should + * send instead of the requested capability message. DO NOT send that + * message: that will be handled by the caller. + */ + virtual void buildMessage(LLMessageSystem* messageSystem, + const LLUUID& agentID, + const LLUUID& sessionID, + const std::string& capabilityName, + const LLSD& payload) const = 0; + /** + * Override this method if you pass a non-empty @a reply + * LLMessageSystem message name to the constructor: that is, if you + * expect to receive an LLMessageSystem message in response to the + * message you constructed in buildMessage(). If you don't pass a @a + * reply message name, you need not override this method as it won't + * be called. + * + * Using LLMessageSystem message-reading operations, your + * readResponse() override should construct and return an LLSD object + * of the form you expect to receive from the real implementation of + * the capability you intend to invoke, when it finally goes live. + */ + virtual LLSD readResponse(LLMessageSystem* messageSystem) const; + + private: + const std::string mCapName; + const std::string mReplyName; + }; + +private: + /// Bind the LLCapabilityProvider passed to our ctor + const LLCapabilityProvider& mProvider; + + /// Post an event to this LLEventPump to invoke a capability message on + /// the bound LLCapabilityProvider's server + /// (https://wiki.lindenlab.com/wiki/Viewer:Messaging/Messaging_Notes#Capabilities) + LLEventStream mEventPump; + + LLMessageSystem* mMessageSystem; + LLUUID mAgentID, mSessionID; + + /// listener to process capability requests + bool capListener(const LLSD&); + + /// helper class for capListener() + class CapabilityMappers; +}; + +#endif /* ! defined(LL_LLCAPABILITYLISTENER_H) */ diff --git a/indra/newview/llcapabilityprovider.h b/indra/newview/llcapabilityprovider.h new file mode 100644 index 0000000000..0ddb2b6cb9 --- /dev/null +++ b/indra/newview/llcapabilityprovider.h @@ -0,0 +1,39 @@ +/** + * @file llcapabilityprovider.h + * @author Nat Goodspeed + * @date 2009-01-07 + * @brief Interface by which to reference (e.g.) LLViewerRegion to obtain a + * capability. + * + * $LicenseInfo:firstyear=2009&license=viewergpl$ + * Copyright (c) 2009, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_LLCAPABILITYPROVIDER_H) +#define LL_LLCAPABILITYPROVIDER_H + +#include "llhost.h" +#include <string> + +/// Interface for obtaining a capability URL, given a capability name +class LLCapabilityProvider +{ +public: + virtual ~LLCapabilityProvider() {} + /** + * Get a capability URL, given a capability name. Returns empty string if + * no such capability is defined on this provider. + */ + virtual std::string getCapability(const std::string& name) const = 0; + /** + * Get host to which to send that capability request. + */ + virtual LLHost getHost() const = 0; + /** + * Describe this LLCapabilityProvider for logging etc. + */ + virtual std::string getDescription() const = 0; +}; + +#endif /* ! defined(LL_LLCAPABILITYPROVIDER_H) */ diff --git a/indra/newview/llfloatergroups.cpp b/indra/newview/llfloatergroups.cpp index 8cd7297447..9e0b0d7230 100644 --- a/indra/newview/llfloatergroups.cpp +++ b/indra/newview/llfloatergroups.cpp @@ -57,6 +57,8 @@ #include "llviewerwindow.h" #include "llimview.h" +using namespace LLOldEvents; + // static std::map<const LLUUID, LLFloaterGroupPicker*> LLFloaterGroupPicker::sInstances; diff --git a/indra/newview/llfloatergroups.h b/indra/newview/llfloatergroups.h index da1c4e23dd..3eb3e5af52 100644 --- a/indra/newview/llfloatergroups.h +++ b/indra/newview/llfloatergroups.h @@ -84,14 +84,14 @@ protected: static instance_map_t sInstances; }; -class LLPanelGroups : public LLPanel, public LLSimpleListener +class LLPanelGroups : public LLPanel, public LLOldEvents::LLSimpleListener { public: LLPanelGroups(); virtual ~LLPanelGroups(); //LLEventListener - /*virtual*/ bool handleEvent(LLPointer<LLEvent> event, const LLSD& userdata); + /*virtual*/ bool handleEvent(LLPointer<LLOldEvents::LLEvent> event, const LLSD& userdata); // clear the group list, and get a fresh set of info. void reset(); diff --git a/indra/newview/llinventorybridge.cpp b/indra/newview/llinventorybridge.cpp index e4459e38bd..5d09d8748f 100644 --- a/indra/newview/llinventorybridge.cpp +++ b/indra/newview/llinventorybridge.cpp @@ -87,6 +87,8 @@ #include "llselectmgr.h" #include "llfloateropenobject.h" +using namespace LLOldEvents; + // Helpers // bug in busy count inc/dec right now, logic is complex... do we really need it? void inc_busy_count() diff --git a/indra/newview/llnetmap.cpp b/indra/newview/llnetmap.cpp index 2eee54df40..23d32fee81 100644 --- a/indra/newview/llnetmap.cpp +++ b/indra/newview/llnetmap.cpp @@ -68,6 +68,8 @@ #include "llglheaders.h" +using namespace LLOldEvents; + const F32 MAP_SCALE_MIN = 64; const F32 MAP_SCALE_MID = 172; const F32 MAP_SCALE_MAX = 512; diff --git a/indra/newview/llnetmap.h b/indra/newview/llnetmap.h index a5bf9f0fb5..9fbd5097fd 100644 --- a/indra/newview/llnetmap.h +++ b/indra/newview/llnetmap.h @@ -109,31 +109,31 @@ private: class LLScaleMap : public LLMemberListener<LLNetMap> { public: - /*virtual*/ bool handleEvent(LLPointer<LLEvent> event, const LLSD& userdata); + /*virtual*/ bool handleEvent(LLPointer<LLOldEvents::LLEvent> event, const LLSD& userdata); }; class LLStopTracking : public LLMemberListener<LLNetMap> { public: - /*virtual*/ bool handleEvent(LLPointer<LLEvent> event, const LLSD& userdata); + /*virtual*/ bool handleEvent(LLPointer<LLOldEvents::LLEvent> event, const LLSD& userdata); }; class LLEnableTracking : public LLMemberListener<LLNetMap> { public: - /*virtual*/ bool handleEvent(LLPointer<LLEvent> event, const LLSD& userdata); + /*virtual*/ bool handleEvent(LLPointer<LLOldEvents::LLEvent> event, const LLSD& userdata); }; class LLShowAgentProfile : public LLMemberListener<LLNetMap> { public: - /*virtual*/ bool handleEvent(LLPointer<LLEvent> event, const LLSD& userdata); + /*virtual*/ bool handleEvent(LLPointer<LLOldEvents::LLEvent> event, const LLSD& userdata); }; class LLEnableProfile : public LLMemberListener<LLNetMap> { public: - /*virtual*/ bool handleEvent(LLPointer<LLEvent> event, const LLSD& userdata); + /*virtual*/ bool handleEvent(LLPointer<LLOldEvents::LLEvent> event, const LLSD& userdata); }; }; diff --git a/indra/newview/llviewercontrol.h b/indra/newview/llviewercontrol.h index d0dc80cb9a..c4003111d3 100644 --- a/indra/newview/llviewercontrol.h +++ b/indra/newview/llviewercontrol.h @@ -84,7 +84,7 @@ class LLCachedControl { T mCachedValue; LLPointer<LLControlVariable> mControl; - boost::signals::connection mConnection; + boost::signals2::scoped_connection mConnection; public: LLCachedControl(const std::string& name, @@ -109,17 +109,13 @@ public: } // Add a listener to the controls signal... - mControl->getSignal()->connect( + mConnection = mControl->getSignal()->connect( boost::bind(&LLCachedControl<T>::handleValueChange, this, _1) ); } ~LLCachedControl() { - if(mConnection.connected()) - { - mConnection.disconnect(); - } } LLCachedControl& operator =(const T& newvalue) diff --git a/indra/newview/llviewerinventory.cpp b/indra/newview/llviewerinventory.cpp index 7f1f0fe6d0..f47d0777b0 100644 --- a/indra/newview/llviewerinventory.cpp +++ b/indra/newview/llviewerinventory.cpp @@ -764,56 +764,39 @@ void move_inventory_item( gAgent.sendReliableMessage(); } -class LLCopyInventoryFromNotecardResponder : public LLHTTPClient::Responder -{ -public: - //If we get back a normal response, handle it here - virtual void result(const LLSD& content) - { - // What do we do here? - llinfos << "CopyInventoryFromNotecard request successful." << llendl; - } - - //If we get back an error (not found, etc...), handle it here - virtual void error(U32 status, const std::string& reason) - { - llinfos << "LLCopyInventoryFromNotecardResponder::error " - << status << ": " << reason << llendl; - } -}; - void copy_inventory_from_notecard(const LLUUID& object_id, const LLUUID& notecard_inv_id, const LLInventoryItem *src, U32 callback_id) { - LLSD body; LLViewerRegion* viewer_region = NULL; - if(object_id.notNull()) - { - LLViewerObject* vo = gObjectList.findObject(object_id); - if(vo) - { - viewer_region = vo->getRegion(); - } + LLViewerObject* vo = NULL; + if (object_id.notNull() && (vo = gObjectList.findObject(object_id)) != NULL) + { + viewer_region = vo->getRegion(); } // Fallback to the agents region if for some reason the // object isn't found in the viewer. - if(!viewer_region) + if (! viewer_region) { viewer_region = gAgent.getRegion(); } - if(viewer_region) + if (! viewer_region) { - std::string url = viewer_region->getCapability("CopyInventoryFromNotecard"); - if (!url.empty()) - { - body["notecard-id"] = notecard_inv_id; - body["object-id"] = object_id; - body["item-id"] = src->getUUID(); - body["folder-id"] = gInventory.findCategoryUUIDForType(src->getType()); - body["callback-id"] = (LLSD::Integer)callback_id; - - LLHTTPClient::post(url, body, new LLCopyInventoryFromNotecardResponder()); - } - } + LL_WARNS("copy_inventory_from_notecard") << "Can't find region from object_id " + << object_id << " or gAgent" + << LL_ENDL; + return; + } + + LLSD request, body; + body["notecard-id"] = notecard_inv_id; + body["object-id"] = object_id; + body["item-id"] = src->getUUID(); + body["folder-id"] = gInventory.findCategoryUUIDForType(src->getType()); + body["callback-id"] = (LLSD::Integer)callback_id; + + request["message"] = "CopyInventoryFromNotecard"; + request["payload"] = body; + + viewer_region->getCapAPI().post(request); } diff --git a/indra/newview/llviewermedia.cpp b/indra/newview/llviewermedia.cpp index 70fc78d277..82f8359732 100644 --- a/indra/newview/llviewermedia.cpp +++ b/indra/newview/llviewermedia.cpp @@ -46,7 +46,7 @@ #include "lluuid.h" #include <boost/bind.hpp> // for SkinFolder listener -#include <boost/signal.hpp> +#include <boost/signals2.hpp> // Implementation functions not exported into header file diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp index fe91da05fa..ac36cf7bb6 100644 --- a/indra/newview/llviewermenu.cpp +++ b/indra/newview/llviewermenu.cpp @@ -1,4 +1,3 @@ - /** * @file llviewermenu.cpp * @brief Builds menus out of items. @@ -217,6 +216,8 @@ #include "lltexlayer.h" using namespace LLVOAvatarDefines; +using namespace LLOldEvents; + void init_client_menu(LLMenuGL* menu); void init_server_menu(LLMenuGL* menu); diff --git a/indra/newview/llviewermenufile.cpp b/indra/newview/llviewermenufile.cpp index 5240fd3211..d1f50212db 100644 --- a/indra/newview/llviewermenufile.cpp +++ b/indra/newview/llviewermenufile.cpp @@ -71,6 +71,8 @@ // system libraries #include <boost/tokenizer.hpp> +using namespace LLOldEvents; + typedef LLMemberListener<LLView> view_listener_t; diff --git a/indra/newview/llviewerregion.cpp b/indra/newview/llviewerregion.cpp index 12d9c1a992..d4ee7a7e50 100644 --- a/indra/newview/llviewerregion.cpp +++ b/indra/newview/llviewerregion.cpp @@ -65,6 +65,11 @@ #include "llvoclouds.h" #include "llworld.h" #include "llspatialpartition.h" +#include "stringize.h" + +#ifdef LL_WINDOWS + #pragma warning(disable:4355) +#endif // Viewer object cache version, change if object update // format changes. JC @@ -172,7 +177,18 @@ LLViewerRegion::LLViewerRegion(const U64 &handle, mCacheEntriesCount(0), mCacheID(), mEventPoll(NULL), - mReleaseNotesRequested(FALSE) + mReleaseNotesRequested(FALSE), + // I'd prefer to set the LLCapabilityListener name to match the region + // name -- it's disappointing that's not available at construction time. + // We could instead store an LLCapabilityListener*, making + // setRegionNameAndZone() replace the instance. Would that pose + // consistency problems? Can we even request a capability before calling + // setRegionNameAndZone()? + // For testability -- the new Michael Feathers paradigm -- + // LLCapabilityListener binds all the globals it expects to need at + // construction time. + mCapabilityListener(host.getString(), gMessageSystem, *this, + gAgent.getID(), gAgent.getSessionID()) { mWidth = region_width_meters; mOriginGlobal = from_region_handle(handle); @@ -223,7 +239,6 @@ LLViewerRegion::LLViewerRegion(const U64 &handle, mObjectPartition.push_back(new LLBridgePartition()); //PARTITION_BRIDGE mObjectPartition.push_back(new LLHUDParticlePartition());//PARTITION_HUD_PARTICLE mObjectPartition.push_back(NULL); //PARTITION_NONE - } @@ -774,6 +789,15 @@ std::ostream& operator<<(std::ostream &s, const LLViewerRegion ®ion) s << "{ "; s << region.mHost; s << " mOriginGlobal = " << region.getOriginGlobal()<< "\n"; + std::string name(region.getName()), zone(region.getZoning()); + if (! name.empty()) + { + s << " mName = " << name << '\n'; + } + if (! zone.empty()) + { + s << " mZoning = " << zone << '\n'; + } s << "}"; return s; } @@ -1518,3 +1542,8 @@ void LLViewerRegion::showReleaseNotes() LLWeb::loadURL(url); mReleaseNotesRequested = FALSE; } + +std::string LLViewerRegion::getDescription() const +{ + return stringize(*this); +} diff --git a/indra/newview/llviewerregion.h b/indra/newview/llviewerregion.h index b0a98a99b8..4d345c811a 100644 --- a/indra/newview/llviewerregion.h +++ b/indra/newview/llviewerregion.h @@ -49,6 +49,8 @@ #include "lldatapacker.h" #include "llvocache.h" #include "llweb.h" +#include "llcapabilityprovider.h" +#include "llcapabilitylistener.h" // Surface id's #define LAND 1 @@ -66,8 +68,9 @@ class LLSurface; class LLVOCache; class LLVOCacheEntry; class LLSpatialPartition; +class LLEventPump; -class LLViewerRegion +class LLViewerRegion: public LLCapabilityProvider // implements this interface { public: //MUST MATCH THE ORDER OF DECLARATION IN CONSTRUCTOR @@ -226,11 +229,19 @@ public: // Get/set named capability URLs for this region. void setSeedCapability(const std::string& url); void setCapability(const std::string& name, const std::string& url); - std::string getCapability(const std::string& name) const; + // implements LLCapabilityProvider + virtual std::string getCapability(const std::string& name) const; static bool isSpecialCapabilityName(const std::string &name); void logActiveCapabilities() const; - const LLHost &getHost() const { return mHost; } + /// Capability-request exception + typedef LLCapabilityListener::ArgError ArgError; + /// Get LLEventPump on which we listen for capability requests + /// (https://wiki.lindenlab.com/wiki/Viewer:Messaging/Messaging_Notes#Capabilities) + LLEventPump& getCapAPI() { return mCapabilityListener.getCapAPI(); } + + /// implements LLCapabilityProvider + virtual LLHost getHost() const { return mHost; } const U64 &getHandle() const { return mHandle; } LLSurface &getLand() const { return *mLandp; } @@ -274,6 +285,8 @@ public: void calculateCameraDistance(); friend std::ostream& operator<<(std::ostream &s, const LLViewerRegion ®ion); + /// implements LLCapabilityProvider + virtual std::string getDescription() const; // used by LCD to get details for debug screen U32 getNetDetailsForLCD(); @@ -394,6 +407,11 @@ private: LLEventPoll* mEventPoll; + /// Post an event to this LLCapabilityListener to invoke a capability message on + /// this LLViewerRegion's server + /// (https://wiki.lindenlab.com/wiki/Viewer:Messaging/Messaging_Notes#Capabilities) + LLCapabilityListener mCapabilityListener; + private: bool mAlive; // can become false if circuit disconnects @@ -461,5 +479,3 @@ inline BOOL LLViewerRegion::getReleaseNotesRequested() const } #endif - - diff --git a/indra/newview/llviewerthrottle.cpp b/indra/newview/llviewerthrottle.cpp index bf779c427a..73065c5c00 100644 --- a/indra/newview/llviewerthrottle.cpp +++ b/indra/newview/llviewerthrottle.cpp @@ -40,6 +40,8 @@ #include "llviewerstats.h" #include "lldatapacker.h" +using namespace LLOldEvents; + // consts // The viewer is allowed to set the under-the-hood bandwidth to 50% diff --git a/indra/newview/tests/llcapabilitylistener_test.cpp b/indra/newview/tests/llcapabilitylistener_test.cpp new file mode 100644 index 0000000000..3c5f6fad2d --- /dev/null +++ b/indra/newview/tests/llcapabilitylistener_test.cpp @@ -0,0 +1,274 @@ +/** + * @file llcapabilitylistener_test.cpp + * @author Nat Goodspeed + * @date 2008-12-31 + * @brief Test for llcapabilitylistener.cpp. + * + * $LicenseInfo:firstyear=2008&license=internal$ + * Copyright (c) 2008, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "../llviewerprecompiledheaders.h" +// Own header +#include "../llcapabilitylistener.h" +// STL headers +#include <stdexcept> +#include <map> +#include <vector> +// std headers +// external library headers +#include "boost/bind.hpp" +// other Linden headers +#include "../test/lltut.h" +#include "../llcapabilityprovider.h" +#include "lluuid.h" +#include "llerrorcontrol.h" +#include "tests/networkio.h" +#include "tests/commtest.h" +#include "stringize.h" + +#if defined(LL_WINDOWS) +#pragma warning(disable: 4355) // using 'this' in base-class ctor initializer expr +#endif + +/***************************************************************************** +* TestCapabilityProvider +*****************************************************************************/ +struct TestCapabilityProvider: public LLCapabilityProvider +{ + TestCapabilityProvider(const LLHost& host): + mHost(host) + {} + + std::string getCapability(const std::string& cap) const + { + CapMap::const_iterator found = mCaps.find(cap); + if (found != mCaps.end()) + return found->second; + // normal LLViewerRegion lookup failure mode + return ""; + } + void setCapability(const std::string& cap, const std::string& url) + { + mCaps[cap] = url; + } + LLHost getHost() const { return mHost; } + std::string getDescription() const { return "TestCapabilityProvider"; } + + LLHost mHost; + typedef std::map<std::string, std::string> CapMap; + CapMap mCaps; +}; + +/***************************************************************************** +* Dummy LLMessageSystem methods +*****************************************************************************/ +/*==========================================================================*| +// This doesn't work because we're already linking in llmessage.a, and we get +// duplicate-symbol errors from the linker. Perhaps if I wanted to go through +// the exercise of providing dummy versions of every single symbol defined in +// message.o -- maybe some day. +typedef std::vector< std::pair<std::string, std::string> > StringPairVector; +StringPairVector call_history; + +S32 LLMessageSystem::sendReliable(const LLHost& host) +{ + call_history.push_back(StringPairVector::value_type("sendReliable", stringize(host))); + return 0; +} +|*==========================================================================*/ + +/***************************************************************************** +* TUT +*****************************************************************************/ +namespace tut +{ + struct llcapears_data: public commtest_data + { + TestCapabilityProvider provider; + LLCapabilityListener regionListener; + LLEventPump& regionPump; + + llcapears_data(): + provider(host), + regionListener("testCapabilityListener", NULL, provider, LLUUID(), LLUUID()), + regionPump(regionListener.getCapAPI()) + { + provider.setCapability("good", server + "capability-test"); + provider.setCapability("fail", server + "fail"); + } + }; + typedef test_group<llcapears_data> llcapears_group; + typedef llcapears_group::object llcapears_object; + llcapears_group llsdmgr("llcapabilitylistener"); + + struct CaptureError: public LLError::OverrideFatalFunction + { + CaptureError(): + LLError::OverrideFatalFunction(boost::bind(&CaptureError::operator(), this, _1)) + { + LLError::setPrintLocation(false); + } + + struct FatalException: public std::runtime_error + { + FatalException(const std::string& what): std::runtime_error(what) {} + }; + + void operator()(const std::string& message) + { + error = message; + throw FatalException(message); + } + + std::string error; + }; + + template<> template<> + void llcapears_object::test<1>() + { + LLSD request, body; + body["data"] = "yes"; + request["payload"] = body; + request["reply"] = replyPump.getName(); + request["error"] = errorPump.getName(); + std::string threw; + try + { + CaptureError capture; + regionPump.post(request); + } + catch (const CaptureError::FatalException& e) + { + threw = e.what(); + } + ensure_contains("missing capability name", threw, "without 'message' key"); + } + + template<> template<> + void llcapears_object::test<2>() + { + LLSD request, body; + body["data"] = "yes"; + request["message"] = "good"; + request["payload"] = body; + request["reply"] = replyPump.getName(); + request["error"] = errorPump.getName(); + regionPump.post(request); + ensure("got response", netio.pump()); + ensure("success response", success); + ensure_equals(result.asString(), "success"); + + body["status"] = 499; + body["reason"] = "custom error message"; + request["message"] = "fail"; + request["payload"] = body; + regionPump.post(request); + ensure("got response", netio.pump()); + ensure("failure response", ! success); + ensure_equals(result["status"].asInteger(), body["status"].asInteger()); + ensure_equals(result["reason"].asString(), body["reason"].asString()); + } + + template<> template<> + void llcapears_object::test<3>() + { + LLSD request, body; + body["data"] = "yes"; + request["message"] = "unknown"; + request["payload"] = body; + request["reply"] = replyPump.getName(); + request["error"] = errorPump.getName(); + std::string threw; + try + { + CaptureError capture; + regionPump.post(request); + } + catch (const CaptureError::FatalException& e) + { + threw = e.what(); + } + ensure_contains("bad capability name", threw, "unsupported capability"); + } + + struct TestMapper: public LLCapabilityListener::CapabilityMapper + { + // Instantiator gets to specify whether mapper expects a reply. + // I'd really like to be able to test CapabilityMapper::buildMessage() + // functionality, too, but -- even though LLCapabilityListener accepts + // the LLMessageSystem* that it passes to CapabilityMapper -- + // LLMessageSystem::sendReliable(const LLHost&) isn't virtual, so it's + // not helpful to pass a subclass instance. I suspect that making any + // LLMessageSystem methods virtual would provoke howls of outrage, + // given how heavily it's used. Nor can I just provide a local + // definition of LLMessageSystem::sendReliable(const LLHost&) because + // we're already linking in the rest of message.o via llmessage.a, and + // that produces duplicate-symbol link errors. + TestMapper(const std::string& replyMessage = std::string()): + LLCapabilityListener::CapabilityMapper("test", replyMessage) + {} + virtual void buildMessage(LLMessageSystem* msg, + const LLUUID& agentID, + const LLUUID& sessionID, + const std::string& capabilityName, + const LLSD& payload) const + { + msg->newMessageFast(_PREHASH_SetStartLocationRequest); + msg->nextBlockFast( _PREHASH_AgentData); + msg->addUUIDFast(_PREHASH_AgentID, agentID); + msg->addUUIDFast(_PREHASH_SessionID, sessionID); + msg->nextBlockFast( _PREHASH_StartLocationData); + // corrected by sim + msg->addStringFast(_PREHASH_SimName, ""); + msg->addU32Fast(_PREHASH_LocationID, payload["HomeLocation"]["LocationId"].asInteger()); +/*==========================================================================*| + msg->addVector3Fast(_PREHASH_LocationPos, + ll_vector3_from_sdmap(payload["HomeLocation"]["LocationPos"])); + msg->addVector3Fast(_PREHASH_LocationLookAt, + ll_vector3_from_sdmap(payload["HomeLocation"]["LocationLookAt"])); +|*==========================================================================*/ + } + }; + + template<> template<> + void llcapears_object::test<4>() + { + TestMapper testMapper("WantReply"); + LLSD request, body; + body["data"] = "yes"; + request["message"] = "test"; + request["payload"] = body; + request["reply"] = replyPump.getName(); + request["error"] = errorPump.getName(); + std::string threw; + try + { + CaptureError capture; + regionPump.post(request); + } + catch (const CaptureError::FatalException& e) + { + threw = e.what(); + } + ensure_contains("capability mapper wants reply", threw, "unimplemented support for reply message"); + } + + template<> template<> + void llcapears_object::test<5>() + { + TestMapper testMapper; + std::string threw; + try + { + TestMapper testMapper2; + } + catch (const std::runtime_error& e) + { + threw = e.what(); + } + ensure_contains("no dup cap mapper", threw, "DupCapMapper"); + } +} diff --git a/indra/test/CMakeLists.txt b/indra/test/CMakeLists.txt index c8682c8ea7..49a0a8f361 100644 --- a/indra/test/CMakeLists.txt +++ b/indra/test/CMakeLists.txt @@ -13,6 +13,7 @@ include(LLXML) include(LScript) include(Linking) include(Tut) +include(Boost) include_directories( ${LLCOMMON_INCLUDE_DIRS} @@ -34,7 +35,9 @@ set(test_SOURCE_FILES llblowfish_tut.cpp llbuffer_tut.cpp lldate_tut.cpp + lldependencies_tut.cpp llerror_tut.cpp + llevents_tut.cpp llhost_tut.cpp llhttpdate_tut.cpp llhttpclient_tut.cpp @@ -71,6 +74,7 @@ set(test_SOURCE_FILES math.cpp message_tut.cpp reflection_tut.cpp + stringize_tut.cpp test.cpp v2math_tut.cpp v3color_tut.cpp @@ -104,7 +108,7 @@ endif (NOT DARWIN) set_source_files_properties(${test_HEADER_FILES} PROPERTIES HEADER_FILE_ONLY TRUE) -list(APPEND test_SOURC_FILES ${test_HEADER_FILES}) +list(APPEND test_SOURCE_FILES ${test_HEADER_FILES}) add_executable(test ${test_SOURCE_FILES}) diff --git a/indra/test/llevents_tut.cpp b/indra/test/llevents_tut.cpp new file mode 100644 index 0000000000..e401f89b22 --- /dev/null +++ b/indra/test/llevents_tut.cpp @@ -0,0 +1,793 @@ +/** + * @file llevents_tut.cpp + * @author Nat Goodspeed + * @date 2008-09-12 + * @brief Test of llevents.h + * + * $LicenseInfo:firstyear=2008&license=viewergpl$ + * Copyright (c) 2008, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if LL_WINDOWS +#pragma warning (disable : 4675) // "resolved by ADL" -- just as I want! +#endif + +// Precompiled header +#include "linden_common.h" +// associated header +// UGLY HACK! We want to verify state internal to the classes without +// providing public accessors. +#define testable public +#include "llevents.h" +#undef testable +// STL headers +// std headers +#include <iostream> +#include <typeinfo> +// external library headers +#include <boost/bind.hpp> +#include <boost/shared_ptr.hpp> +#include <boost/assign/list_of.hpp> +// other Linden headers +#include "lltut.h" +#include "stringize.h" + +using boost::assign::list_of; + +/***************************************************************************** +* test listener class +*****************************************************************************/ +class Listener; +std::ostream& operator<<(std::ostream&, const Listener&); + +class Listener +{ +public: + Listener(const std::string& name): + mName(name) + { +// std::cout << *this << ": ctor\n"; + } + Listener(const Listener& that): + mName(that.mName), + mLastEvent(that.mLastEvent) + { +// std::cout << *this << ": copy\n"; + } + virtual ~Listener() + { +// std::cout << *this << ": dtor\n"; + } + std::string getName() const { return mName; } + bool call(const LLSD& event) + { +// std::cout << *this << "::call(" << event << ")\n"; + mLastEvent = event; + return false; + } + bool callstop(const LLSD& event) + { +// std::cout << *this << "::callstop(" << event << ")\n"; + mLastEvent = event; + return true; + } + LLSD getLastEvent() const + { +// std::cout << *this << "::getLastEvent() -> " << mLastEvent << "\n"; + return mLastEvent; + } + void reset(const LLSD& to = LLSD()) + { +// std::cout << *this << "::reset(" << to << ")\n"; + mLastEvent = to; + } + +private: + std::string mName; + LLSD mLastEvent; +}; + +std::ostream& operator<<(std::ostream& out, const Listener& listener) +{ + out << "Listener(" << listener.getName() /* << "@" << &listener */ << ')'; + return out; +} + +struct Collect +{ + bool add(const std::string& bound, const LLSD& event) + { + result.push_back(bound); + return false; + } + void clear() { result.clear(); } + typedef std::vector<std::string> StringList; + StringList 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; +} + +template<typename T> +T make(const T& value) { return value; } + +/***************************************************************************** +* tut test group +*****************************************************************************/ +namespace tut +{ + struct events_data + { + events_data(): + pumps(LLEventPumps::instance()), + listener0("first"), + listener1("second") + {} + LLEventPumps& pumps; + Listener listener0; + Listener listener1; + + void check_listener(const std::string& desc, const Listener& listener, LLSD::Integer got) + { + ensure_equals(STRINGIZE(listener << ' ' << desc), + listener.getLastEvent().asInteger(), got); + } + }; + typedef test_group<events_data> events_group; + typedef events_group::object events_object; + tut::events_group evgr("events"); + + template<> template<> + void events_object::test<1>() + { + set_test_name("basic operations"); + // Now there's a static constructor in llevents.cpp that registers on + // the "mainloop" pump to call LLEventPumps::flush(). + // Actually -- having to modify this to track the statically- + // constructed pumps in other TUT modules in this giant monolithic test + // executable isn't such a hot idea. +// ensure_equals("initial pump", pumps.mPumpMap.size(), 1); + size_t initial_pumps(pumps.mPumpMap.size()); + LLEventPump& per_frame(pumps.obtain("per-frame")); + ensure_equals("first explicit pump", pumps.mPumpMap.size(), initial_pumps+1); + // Verify that per_frame was instantiated as an LLEventStream. + ensure("LLEventStream leaf class", dynamic_cast<LLEventStream*>(&per_frame)); + ensure("enabled", per_frame.enabled()); + // Trivial test, but posting an event to an EventPump with no + // listeners should not blow up. The test is relevant because defining + // a boost::signal with a non-void return signature, using the default + // combiner, blows up if there are no listeners. This is because the + // default combiner is defined to return the value returned by the + // last listener, which is meaningless if there were no listeners. + per_frame.post(0); + // NOTE: boost::bind() saves its arguments by VALUE! If you pass an + // object instance rather than a pointer, you'll end up binding to an + // internal copy of that instance! Use boost::ref() to capture a + // reference instead. + LLBoundListener connection = per_frame.listen(listener0.getName(), + boost::bind(&Listener::call, + boost::ref(listener0), + _1)); + ensure("connected", connection.connected()); + ensure("not blocked", ! connection.blocked()); + per_frame.post(1); + check_listener("received", listener0, 1); + { // block the connection + LLEventPump::Blocker block(connection); + ensure("blocked", connection.blocked()); + per_frame.post(2); + check_listener("not updated", listener0, 1); + } // unblock + ensure("unblocked", ! connection.blocked()); + per_frame.post(3); + check_listener("unblocked", listener0, 3); + LLBoundListener sameConnection = per_frame.getListener(listener0.getName()); + ensure("still connected", sameConnection.connected()); + ensure("still not blocked", ! sameConnection.blocked()); + { // block it again + LLEventPump::Blocker block(sameConnection); + ensure("re-blocked", sameConnection.blocked()); + per_frame.post(4); + check_listener("re-blocked", listener0, 3); + } // unblock + bool threw = false; + try + { + per_frame.listen(listener0.getName(), // note bug, dup name + boost::bind(&Listener::call, boost::ref(listener1), _1)); + } + catch (const LLEventPump::DupListenerName& e) + { + threw = true; + ensure_equals(e.what(), + std::string("DupListenerName: " + "Attempt to register duplicate listener name '") + + listener0.getName() + + "' on " + typeid(per_frame).name() + " '" + per_frame.getName() + "'"); + } + ensure("threw DupListenerName", threw); + // do it right this time + per_frame.listen(listener1.getName(), + boost::bind(&Listener::call, boost::ref(listener1), _1)); + per_frame.post(5); + check_listener("got", listener0, 5); + check_listener("got", listener1, 5); + per_frame.enable(false); + per_frame.post(6); + check_listener("didn't get", listener0, 5); + check_listener("didn't get", listener1, 5); + per_frame.enable(); + per_frame.post(7); + check_listener("got", listener0, 7); + check_listener("got", listener1, 7); + per_frame.stopListening(listener0.getName()); + ensure("disconnected 0", ! connection.connected()); + ensure("disconnected 1", ! sameConnection.connected()); + per_frame.post(8); + check_listener("disconnected", listener0, 7); + check_listener("still connected", listener1, 8); + per_frame.stopListening(listener1.getName()); + per_frame.post(9); + check_listener("disconnected", listener1, 8); + } + + template<> template<> + void events_object::test<2>() + { + set_test_name("callstop() returning true"); + LLEventPump& per_frame(pumps.obtain("per-frame")); + listener0.reset(0); + listener1.reset(0); + LLBoundListener bound0 = per_frame.listen(listener0.getName(), + boost::bind(&Listener::callstop, + boost::ref(listener0), + _1)); + LLBoundListener bound1 = per_frame.listen(listener1.getName(), + boost::bind(&Listener::call, + boost::ref(listener1), + _1), + // after listener0 + make<LLEventPump::NameList>(list_of(listener0.getName()))); + ensure("enabled", per_frame.enabled()); + ensure("connected 0", bound0.connected()); + ensure("unblocked 0", ! bound0.blocked()); + ensure("connected 1", bound1.connected()); + ensure("unblocked 1", ! bound1.blocked()); + per_frame.post(1); + check_listener("got", listener0, 1); + // Because listener0.callstop() returns true, control never reaches listener1.call(). + check_listener("got", listener1, 0); + } + + bool chainEvents(Listener& someListener, const LLSD& event) + { + // Make this call so we can watch for side effects for test purposes. + someListener.call(event); + // This function represents a recursive event chain -- or some other + // scenario in which an event handler raises additional events. + int value = event.asInteger(); + if (value) + { + LLEventPumps::instance().obtain("login").post(value - 1); + } + return false; + } + + template<> template<> + void events_object::test<3>() + { + set_test_name("LLEventQueue delayed action"); + // This access is NOT legal usage: we can do it only because we're + // hacking private for test purposes. Normally we'd either compile in + // a particular name, or (later) edit a config file. + pumps.mQueueNames.insert("login"); + LLEventPump& login(pumps.obtain("login")); + // The "mainloop" pump is special: posting on that implicitly calls + // LLEventPumps::flush(), which in turn should flush our "login" + // LLEventQueue. + LLEventPump& mainloop(pumps.obtain("mainloop")); + ensure("LLEventQueue leaf class", dynamic_cast<LLEventQueue*>(&login)); + login.listen(listener0.getName(), boost::bind(&Listener::call, boost::ref(listener0), _1)); + listener0.reset(0); + login.post(1); + check_listener("waiting for queued event", listener0, 0); + mainloop.post(LLSD()); + check_listener("got queued event", listener0, 1); + login.stopListening(listener0.getName()); + // Verify that when an event handler posts a new event on the same + // LLEventQueue, it doesn't get processed in the same flush() call -- + // it waits until the next flush() call. + listener0.reset(17); + login.listen("chainEvents", boost::bind(chainEvents, boost::ref(listener0), _1)); + login.post(1); + check_listener("chainEvents(1) not yet called", listener0, 17); + mainloop.post(LLSD()); + check_listener("chainEvents(1) called", listener0, 1); + mainloop.post(LLSD()); + check_listener("chainEvents(0) called", listener0, 0); + mainloop.post(LLSD()); + check_listener("chainEvents(-1) not called", listener0, 0); + login.stopListening("chainEvents"); + } + + template<> template<> + void events_object::test<4>() + { + set_test_name("explicitly-instantiated LLEventStream"); + // Explicitly instantiate an LLEventStream, and verify that it + // self-registers with LLEventPumps + size_t registered = pumps.mPumpMap.size(); + size_t owned = pumps.mOurPumps.size(); + LLEventPump* localInstance; + { + LLEventStream myEventStream("stream"); + localInstance = &myEventStream; + LLEventPump& stream(pumps.obtain("stream")); + ensure("found named LLEventStream instance", &stream == localInstance); + ensure_equals("registered new instance", pumps.mPumpMap.size(), registered + 1); + ensure_equals("explicit instance not owned", pumps.mOurPumps.size(), owned); + } // destroy myEventStream -- should unregister + ensure_equals("destroyed instance unregistered", pumps.mPumpMap.size(), registered); + ensure_equals("destroyed instance not owned", pumps.mOurPumps.size(), owned); + LLEventPump& stream(pumps.obtain("stream")); + ensure("new LLEventStream instance", &stream != localInstance); + ensure_equals("obtain()ed instance registered", pumps.mPumpMap.size(), registered + 1); + ensure_equals("obtain()ed instance owned", pumps.mOurPumps.size(), owned + 1); + } + + template<> template<> + void events_object::test<5>() + { + set_test_name("stopListening()"); + LLEventPump& login(pumps.obtain("login")); + login.listen(listener0.getName(), boost::bind(&Listener::call, boost::ref(listener0), _1)); + login.stopListening(listener0.getName()); + // should not throw because stopListening() should have removed name + login.listen(listener0.getName(), + boost::bind(&Listener::callstop, boost::ref(listener0), _1)); + LLBoundListener wrong = login.getListener("bogus"); + ensure("bogus connection disconnected", ! wrong.connected()); + ensure("bogus connection blocked", wrong.blocked()); + } + + template<> template<> + void events_object::test<6>() + { + set_test_name("chaining LLEventPump instances"); + LLEventPump& upstream(pumps.obtain("upstream")); + // One potentially-useful construct is to chain LLEventPumps together. + // Among other things, this allows you to turn subsets of listeners on + // and off in groups. + LLEventPump& filter0(pumps.obtain("filter0")); + LLEventPump& filter1(pumps.obtain("filter1")); + upstream.listen(filter0.getName(), + boost::bind(&LLEventPump::post, boost::ref(filter0), _1)); + upstream.listen(filter1.getName(), + boost::bind(&LLEventPump::post, boost::ref(filter1), _1)); + filter0.listen(listener0.getName(), + boost::bind(&Listener::call, boost::ref(listener0), _1)); + filter1.listen(listener1.getName(), + boost::bind(&Listener::call, boost::ref(listener1), _1)); + listener0.reset(0); + listener1.reset(0); + upstream.post(1); + check_listener("got unfiltered", listener0, 1); + check_listener("got unfiltered", listener1, 1); + filter0.enable(false); + upstream.post(2); + check_listener("didn't get filtered", listener0, 1); + check_listener("got filtered", listener1, 2); + } + + template<> template<> + void events_object::test<7>() + { + set_test_name("listener dependency order"); + typedef LLEventPump::NameList NameList; + typedef Collect::StringList StringList; + LLEventPump& button(pumps.obtain("button")); + Collect collector; + button.listen("Mary", + boost::bind(&Collect::add, boost::ref(collector), "Mary", _1), + // state that "Mary" must come after "checked" + make<NameList>(list_of("checked"))); + button.listen("checked", + boost::bind(&Collect::add, boost::ref(collector), "checked", _1), + // "checked" must come after "spot" + make<NameList>(list_of("spot"))); + button.listen("spot", + boost::bind(&Collect::add, boost::ref(collector), "spot", _1)); + button.post(1); + ensure_equals(collector.result, make<StringList>(list_of("spot")("checked")("Mary"))); + collector.clear(); + button.stopListening("Mary"); + button.listen("Mary", + boost::bind(&Collect::add, boost::ref(collector), "Mary", _1), + LLEventPump::empty, // no after dependencies + // now "Mary" must come before "spot" + make<NameList>(list_of("spot"))); + button.post(2); + ensure_equals(collector.result, make<StringList>(list_of("Mary")("spot")("checked"))); + collector.clear(); + button.stopListening("spot"); + std::string threw; + try + { + button.listen("spot", + boost::bind(&Collect::add, boost::ref(collector), "spot", _1), + // after "Mary" and "checked" -- whoops! + make<NameList>(list_of("Mary")("checked"))); + } + catch (const LLEventPump::Cycle& e) + { + threw = e.what(); +// std::cout << "Caught: " << e.what() << '\n'; + } + // Obviously the specific wording of the exception text can + // change; go ahead and change the test to match. + // Establish that it contains: + // - the name and runtime type of the LLEventPump + ensure_contains("LLEventPump type", threw, typeid(button).name()); + ensure_contains("LLEventPump name", threw, "'button'"); + // - the name of the new listener that caused the problem + ensure_contains("new listener name", threw, "'spot'"); + // - a synopsis of the problematic dependencies. + ensure_contains("cyclic dependencies", threw, + "\"Mary\" -> before (\"spot\")"); + ensure_contains("cyclic dependencies", threw, + "after (\"spot\") -> \"checked\""); + ensure_contains("cyclic dependencies", threw, + "after (\"Mary\", \"checked\") -> \"spot\""); + button.listen("yellow", + boost::bind(&Collect::add, boost::ref(collector), "yellow", _1), + make<NameList>(list_of("checked"))); + button.listen("shoelaces", + boost::bind(&Collect::add, boost::ref(collector), "shoelaces", _1), + make<NameList>(list_of("checked"))); + button.post(3); + ensure_equals(collector.result, make<StringList>(list_of("Mary")("checked")("yellow")("shoelaces"))); + collector.clear(); + threw.clear(); + try + { + button.listen("of", + boost::bind(&Collect::add, boost::ref(collector), "of", _1), + make<NameList>(list_of("shoelaces")), + make<NameList>(list_of("yellow"))); + } + catch (const LLEventPump::OrderChange& e) + { + threw = e.what(); +// std::cout << "Caught: " << e.what() << '\n'; + } + // Same remarks about the specific wording of the exception. Just + // ensure that it contains enough information to clarify the + // problem and what must be done to resolve it. + ensure_contains("LLEventPump type", threw, typeid(button).name()); + ensure_contains("LLEventPump name", threw, "'button'"); + ensure_contains("new listener name", threw, "'of'"); + ensure_contains("prev listener name", threw, "'yellow'"); + ensure_contains("old order", threw, "was: Mary, checked, yellow, shoelaces"); + ensure_contains("new order", threw, "now: Mary, checked, shoelaces, of, yellow"); + button.post(4); + ensure_equals(collector.result, make<StringList>(list_of("Mary")("checked")("yellow")("shoelaces"))); + } + + template<> template<> + void events_object::test<8>() + { + set_test_name("tweaked and untweaked LLEventPump instance names"); + { // nested scope + // Hand-instantiate an LLEventStream... + LLEventStream bob("bob"); + bool threw = false; + try + { + // then another with a duplicate name. + LLEventStream bob2("bob"); + } + catch (const LLEventPump::DupPumpName& /*e*/) + { + threw = true; +// std::cout << "Caught: " << e.what() << '\n'; + } + ensure("Caught DupPumpName", threw); + } // delete first 'bob' + LLEventStream bob("bob"); // should work, previous one unregistered + LLEventStream bob1("bob", true); // allowed to tweak name + ensure_equals("tweaked LLEventStream name", bob1.getName(), "bob1"); + std::vector< boost::shared_ptr<LLEventStream> > streams; + for (int i = 2; i <= 10; ++i) + { + streams.push_back(boost::shared_ptr<LLEventStream>(new LLEventStream("bob", true))); + } + ensure_equals("last tweaked LLEventStream name", streams.back()->getName(), "bob10"); + } + + // Define a function that accepts an LLListenerOrPumpName + void eventSource(const LLListenerOrPumpName& listener) + { + // Pretend that some time has elapsed. Call listener immediately. + listener(17); + } + + template<> template<> + void events_object::test<9>() + { + set_test_name("LLListenerOrPumpName"); + // Passing a boost::bind() expression to LLListenerOrPumpName + listener0.reset(0); + eventSource(boost::bind(&Listener::call, boost::ref(listener0), _1)); + check_listener("got by listener", listener0, 17); + // Passing a string LLEventPump name to LLListenerOrPumpName + listener0.reset(0); + LLEventStream random("random"); + random.listen(listener0.getName(), boost::bind(&Listener::call, boost::ref(listener0), _1)); + eventSource("random"); + check_listener("got by pump name", listener0, 17); + bool threw = false; + try + { + LLListenerOrPumpName empty; + empty(17); + } + catch (const LLListenerOrPumpName::Empty&) + { + threw = true; + } + ensure("threw Empty", threw); + } + + class TempListener: public Listener + { + public: + TempListener(const std::string& name, bool& liveFlag): + Listener(name), + mLiveFlag(liveFlag) + { + mLiveFlag = true; + } + + virtual ~TempListener() + { + mLiveFlag = false; + } + + private: + bool& mLiveFlag; + }; + + template<> template<> + void events_object::test<10>() + { + set_test_name("listen(boost::bind(...TempListener...))"); + // listen() can't do anything about a plain TempListener instance: + // it's not managed with shared_ptr, nor is it an LLEventTrackable subclass + bool live = false; + LLEventPump& heaptest(pumps.obtain("heaptest")); + LLBoundListener connection; + { + TempListener tempListener("temp", live); + ensure("TempListener constructed", live); + connection = heaptest.listen(tempListener.getName(), + boost::bind(&Listener::call, + boost::ref(tempListener), + _1)); + heaptest.post(1); + check_listener("received", tempListener, 1); + } // presumably this will make newListener go away? + // verify that + ensure("TempListener destroyed", ! live); + // This is the case against which we can't defend. Don't even try to + // post to heaptest -- that would engage Undefined Behavior. + // Cautiously inspect connection... + ensure("misleadingly connected", connection.connected()); + // then disconnect by hand. + heaptest.stopListening("temp"); + } + + template<> template<> + void events_object::test<11>() + { + set_test_name("listen(boost::bind(...weak_ptr...))"); + // listen() detecting weak_ptr<TempListener> in boost::bind() object + bool live = false; + LLEventPump& heaptest(pumps.obtain("heaptest")); + LLBoundListener connection; + ensure("default state", ! connection.connected()); + { + boost::shared_ptr<TempListener> newListener(new TempListener("heap", live)); + newListener->reset(); + ensure("TempListener constructed", live); + connection = heaptest.listen(newListener->getName(), + boost::bind(&Listener::call, weaken(newListener), _1)); + ensure("new connection", connection.connected()); + heaptest.post(1); + check_listener("received", *newListener, 1); + } // presumably this will make newListener go away? + // verify that + ensure("TempListener destroyed", ! live); + ensure("implicit disconnect", ! connection.connected()); + // now just make sure we don't blow up trying to access a freed object! + heaptest.post(2); + } + + template<> template<> + void events_object::test<12>() + { + set_test_name("listen(boost::bind(...shared_ptr...))"); +/*==========================================================================*| + // DISABLED because I've made this case produce a compile error. + // Following the error leads the disappointed dev to a comment + // instructing her to use the weaken() function to bind a weak_ptr<T> + // instead of binding a shared_ptr<T>, and explaining why. I know of + // no way to use TUT to code a repeatable test in which the expected + // outcome is a compile error. The interested reader is invited to + // uncomment this block and build to see for herself. + + // listen() detecting shared_ptr<TempListener> in boost::bind() object + bool live = false; + LLEventPump& heaptest(pumps.obtain("heaptest")); + LLBoundListener connection; + std::string listenerName("heap"); + ensure("default state", ! connection.connected()); + { + boost::shared_ptr<TempListener> newListener(new TempListener(listenerName, live)); + ensure_equals("use_count", newListener.use_count(), 1); + newListener->reset(); + ensure("TempListener constructed", live); + connection = heaptest.listen(newListener->getName(), + boost::bind(&Listener::call, newListener, _1)); + ensure("new connection", connection.connected()); + ensure_equals("use_count", newListener.use_count(), 2); + heaptest.post(1); + check_listener("received", *newListener, 1); + } // this should make newListener go away... + // Unfortunately, the fact that we've bound a shared_ptr by value into + // our LLEventPump means that copy will keep the referenced object alive. + ensure("TempListener still alive", live); + ensure("still connected", connection.connected()); + // disconnecting explicitly should delete the TempListener... + heaptest.stopListening(listenerName); +#if 0 // however, in my experience, it does not. I don't know why not. + // Ah: on 2009-02-19, Frank Mori Hess, author of the Boost.Signals2 + // library, stated on the boost-users mailing list: + // http://www.nabble.com/Re%3A--signals2--review--The-review-of-the-signals2-library-(formerly-thread_safe_signals)-begins-today%2C-Nov-1st-p22102367.html + // "It will get destroyed eventually. The signal cleans up its slot + // list little by little during connect/invoke. It doesn't immediately + // remove disconnected slots from the slot list since other threads + // might be using the same slot list concurrently. It might be + // possible to make it immediately reset the shared_ptr owning the + // slot though, leaving an empty shared_ptr in the slot list, since + // that wouldn't invalidate any iterators." + ensure("TempListener destroyed", ! live); + ensure("implicit disconnect", ! connection.connected()); +#endif // 0 + // now just make sure we don't blow up trying to access a freed object! + heaptest.post(2); +|*==========================================================================*/ + } + + class TempTrackableListener: public TempListener, public LLEventTrackable + { + public: + TempTrackableListener(const std::string& name, bool& liveFlag): + TempListener(name, liveFlag) + {} + }; + + template<> template<> + void events_object::test<13>() + { + set_test_name("listen(boost::bind(...TempTrackableListener ref...))"); + bool live = false; + LLEventPump& heaptest(pumps.obtain("heaptest")); + LLBoundListener connection; + { + TempTrackableListener tempListener("temp", live); + ensure("TempTrackableListener constructed", live); + connection = heaptest.listen(tempListener.getName(), + boost::bind(&TempTrackableListener::call, + boost::ref(tempListener), _1)); + heaptest.post(1); + check_listener("received", tempListener, 1); + } // presumably this will make tempListener go away? + // verify that + ensure("TempTrackableListener destroyed", ! live); + ensure("implicit disconnect", ! connection.connected()); + // now just make sure we don't blow up trying to access a freed object! + heaptest.post(2); + } + + template<> template<> + void events_object::test<14>() + { + set_test_name("listen(boost::bind(...TempTrackableListener pointer...))"); + bool live = false; + LLEventPump& heaptest(pumps.obtain("heaptest")); + LLBoundListener connection; + { + TempTrackableListener* newListener(new TempTrackableListener("temp", live)); + ensure("TempTrackableListener constructed", live); + connection = heaptest.listen(newListener->getName(), + boost::bind(&TempTrackableListener::call, + newListener, _1)); + heaptest.post(1); + check_listener("received", *newListener, 1); + // explicitly destroy newListener + delete newListener; + } + // verify that + ensure("TempTrackableListener destroyed", ! live); + ensure("implicit disconnect", ! connection.connected()); + // now just make sure we don't blow up trying to access a freed object! + heaptest.post(2); + } + + class TempSharedListener: public TempListener, + public boost::enable_shared_from_this<TempSharedListener> + { + public: + TempSharedListener(const std::string& name, bool& liveFlag): + TempListener(name, liveFlag) + {} + }; + + template<> template<> + void events_object::test<15>() + { + set_test_name("listen(boost::bind(...TempSharedListener ref...))"); +#if 0 + bool live = false; + LLEventPump& heaptest(pumps.obtain("heaptest")); + LLBoundListener connection; + { + // We MUST have at least one shared_ptr to an + // enable_shared_from_this subclass object before + // shared_from_this() can work. + boost::shared_ptr<TempSharedListener> + tempListener(new TempSharedListener("temp", live)); + ensure("TempSharedListener constructed", live); + // However, we're not passing either the shared_ptr or its + // corresponding weak_ptr -- instead, we're passing a reference to + // the TempSharedListener. +/*==========================================================================*| + std::cout << "Capturing const ref" << std::endl; + const boost::enable_shared_from_this<TempSharedListener>& cref(*tempListener); + std::cout << "Capturing const ptr" << std::endl; + const boost::enable_shared_from_this<TempSharedListener>* cp(&cref); + std::cout << "Capturing non-const ptr" << std::endl; + boost::enable_shared_from_this<TempSharedListener>* p(const_cast<boost::enable_shared_from_this<TempSharedListener>*>(cp)); + std::cout << "Capturing shared_from_this()" << std::endl; + boost::shared_ptr<TempSharedListener> sp(p->shared_from_this()); + std::cout << "Capturing weak_ptr" << std::endl; + boost::weak_ptr<TempSharedListener> wp(weaken(sp)); + std::cout << "Binding weak_ptr" << std::endl; +|*==========================================================================*/ + connection = heaptest.listen(tempListener->getName(), + boost::bind(&TempSharedListener::call, *tempListener, _1)); + heaptest.post(1); + check_listener("received", *tempListener, 1); + } // presumably this will make tempListener go away? + // verify that + ensure("TempSharedListener destroyed", ! live); + ensure("implicit disconnect", ! connection.connected()); + // now just make sure we don't blow up trying to access a freed object! + heaptest.post(2); +#endif // 0 + } +} // namespace tut |