diff --git a/indra/CMakeLists.txt b/indra/CMakeLists.txt
index c285bcae4b20da3e63da829994682e4f73ced4f4..e8e05f727bcf2cd53dfdd1a0357b4574c7906dd0 100644
--- a/indra/CMakeLists.txt
+++ b/indra/CMakeLists.txt
@@ -64,6 +64,7 @@ add_custom_target(viewer)
 if (VIEWER)
   add_subdirectory(${LIBS_OPEN_PREFIX}llcrashlogger)
   add_subdirectory(${LIBS_OPEN_PREFIX}llui)
+  add_subdirectory(${LIBS_OPEN_PREFIX}viewer_components)
 
   if (LINUX)
     add_subdirectory(${VIEWER_PREFIX}linux_crash_logger)
diff --git a/indra/cmake/LLAddBuildTest.cmake b/indra/cmake/LLAddBuildTest.cmake
index b19eebe1fed0b3818e8bf3e0b9a074ad21f9dd77..6eeff45afe79799c8b34a7933cd94e9b3ec07edd 100644
--- a/indra/cmake/LLAddBuildTest.cmake
+++ b/indra/cmake/LLAddBuildTest.cmake
@@ -1,6 +1,7 @@
 # -*- cmake -*-
 
 INCLUDE(APR)
+INCLUDE(Pth)
 INCLUDE(LLMath)
 
 MACRO(ADD_BUILD_TEST_NO_COMMON name parent)
@@ -36,6 +37,7 @@ MACRO(ADD_BUILD_TEST name parent)
             ${APR_LIBRARIES}
             ${PTHREAD_LIBRARY}
             ${WINDOWS_LIBRARIES}
+            ${PTH_LIBRARIES}
             )
         SET(basic_source_files
             ${name}.cpp
@@ -89,10 +91,13 @@ MACRO(ADD_BUILD_TEST_INTERNAL name parent libraries source_files)
     GET_TARGET_PROPERTY(TEST_EXE ${name}_test LOCATION)
     SET(TEST_OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${name}_test_ok.txt)
 
+    SET(run_needs ${name}_test)
+
     IF ("${wrapper}" STREQUAL "")
       SET(TEST_CMD ${TEST_EXE} --touch=${TEST_OUTPUT} --sourcedir=${CMAKE_CURRENT_SOURCE_DIR})
     ELSE ("${wrapper}" STREQUAL "")
       SET(TEST_CMD ${PYTHON_EXECUTABLE} ${wrapper} ${TEST_EXE} --touch=${TEST_OUTPUT} --sourcedir=${CMAKE_CURRENT_SOURCE_DIR})
+      SET(run_needs ${run_needs} ${wrapper})
     ENDIF ("${wrapper}" STREQUAL "")
 
     #MESSAGE(STATUS "ADD_BUILD_TEST_INTERNAL ${name} test_cmd  = ${TEST_CMD}")
@@ -107,7 +112,7 @@ MACRO(ADD_BUILD_TEST_INTERNAL name parent libraries source_files)
     ADD_CUSTOM_COMMAND(
         OUTPUT ${TEST_OUTPUT}
         COMMAND ${TEST_SCRIPT_CMD}
-        DEPENDS ${name}_test
+        DEPENDS ${run_needs}
         WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
         )
 
diff --git a/indra/cmake/LLLogin.cmake b/indra/cmake/LLLogin.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..47d171876a00df3fea3bf61a794cb3ff81fcd07b
--- /dev/null
+++ b/indra/cmake/LLLogin.cmake
@@ -0,0 +1,7 @@
+# -*- cmake -*-
+
+set(LLLOGIN_INCLUDE_DIRS
+    ${LIBS_OPEN_DIR}/viewer_components/login
+    )
+
+set(LLLOGIN_LIBRARIES lllogin)
diff --git a/indra/cmake/Pth.cmake b/indra/cmake/Pth.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..a28f6ec696efdbbcaeb8c1ee34dfb7dd7d436421
--- /dev/null
+++ b/indra/cmake/Pth.cmake
@@ -0,0 +1,21 @@
+# -*- cmake -*-
+include(Prebuilt)
+
+set(PTH_FIND_QUIETLY ON)
+set(PTH_FIND_REQUIRED ON)
+
+if (STANDALONE)
+#  ?? How would I construct FindPTH.cmake? This file was cloned from
+#  CURL.cmake, which uses include(FindCURL), but there's no FindCURL.cmake?
+#  include(FindPTH)
+else (STANDALONE)
+  # This library is only needed to support Boost.Coroutine, and only on Mac.
+  if (DARWIN)
+    use_prebuilt_binary(pth)
+    set(PTH_LIBRARIES pth)
+    set(PTH_INCLUDE_DIRS ${LIBS_PREBUILT_DIR}/include)
+  else (DARWIN)
+    set(PTH_LIBRARIES)
+    set(PTH_INCLUDE_DIRS)
+  endif (DARWIN)
+endif (STANDALONE)
diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt
index 694f3d5de83455d5e77dcc76b824ff21fb26ec13..d3d75f78df06f7e2bbfd5f64b16a22b24ed83360 100644
--- a/indra/llcommon/CMakeLists.txt
+++ b/indra/llcommon/CMakeLists.txt
@@ -33,6 +33,8 @@ set(llcommon_SOURCE_FILES
     llerror.cpp
     llerrorthread.cpp
     llevent.cpp
+    lleventcoro.cpp
+    lleventfilter.cpp
     llevents.cpp
     llfasttimer.cpp
     llfile.cpp
@@ -118,6 +120,8 @@ set(llcommon_HEADER_FILES
     llerrorlegacy.h
     llerrorthread.h
     llevent.h
+    lleventcoro.h
+    lleventfilter.h
     llevents.h
     lleventemitter.h
     llextendedstatus.h
@@ -223,3 +227,5 @@ target_link_libraries(
     )
 
 ADD_BUILD_TEST(lllazy llcommon)
+ADD_BUILD_TEST(lleventfilter llcommon)
+ADD_BUILD_TEST(coroutine llcommon)
diff --git a/indra/llcommon/lleventcoro.cpp b/indra/llcommon/lleventcoro.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..cea5a1eda38d7e2c2b8ded00b2cbfd792147fac4
--- /dev/null
+++ b/indra/llcommon/lleventcoro.cpp
@@ -0,0 +1,118 @@
+/**
+ * @file   lleventcoro.cpp
+ * @author Nat Goodspeed
+ * @date   2009-04-29
+ * @brief  Implementation for lleventcoro.
+ * 
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * Copyright (c) 2009, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "lleventcoro.h"
+// STL headers
+#include <map>
+// std headers
+// external library headers
+// other Linden headers
+#include "llsdserialize.h"
+#include "llerror.h"
+
+std::string LLEventDetail::listenerNameForCoro(const void* self)
+{
+    typedef std::map<const void*, std::string> MapType;
+    static MapType memo;
+    MapType::const_iterator found = memo.find(self);
+    if (found != memo.end())
+    {
+        // this coroutine instance has called us before, reuse same name
+        return found->second;
+    }
+    // this is the first time we've been called for this coroutine instance
+    std::string name(LLEventPump::inventName("coro"));
+    memo[self] = name;
+    return name;
+}
+
+void LLEventDetail::storeToLLSDPath(LLSD& dest, const LLSD& rawPath, const LLSD& value)
+{
+    if (rawPath.isUndefined())
+    {
+        // no-op case
+        return;
+    }
+
+    // Arrange to treat rawPath uniformly as an array. If it's not already an
+    // array, store it as the only entry in one.
+    LLSD path;
+    if (rawPath.isArray())
+    {
+        path = rawPath;
+    }
+    else
+    {
+        path.append(rawPath);
+    }
+
+    // Need to indicate a current destination -- but that current destination
+    // needs to change as we step through the path array. Where normally we'd
+    // use an LLSD& to capture a subscripted LLSD lvalue, this time we must
+    // instead use a pointer -- since it must be reassigned.
+    LLSD* pdest = &dest;
+
+    // Now loop through that array
+    for (LLSD::Integer i = 0; i < path.size(); ++i)
+    {
+        if (path[i].isString())
+        {
+            // *pdest is an LLSD map
+            pdest = &((*pdest)[path[i].asString()]);
+        }
+        else if (path[i].isInteger())
+        {
+            // *pdest is an LLSD array
+            pdest = &((*pdest)[path[i].asInteger()]);
+        }
+        else
+        {
+            // What do we do with Real or Array or Map or ...?
+            // As it's a coder error -- not a user error -- rub the coder's
+            // face in it so it gets fixed.
+            LL_ERRS("lleventcoro") << "storeToLLSDPath(" << dest << ", " << rawPath << ", " << value
+                                   << "): path[" << i << "] bad type " << path[i].type() << LL_ENDL;
+        }
+    }
+
+    // Here *pdest is where we should store value.
+    *pdest = value;
+}
+
+LLSD errorException(const LLEventWithID& result, const std::string& desc)
+{
+    // If the result arrived on the error pump (pump 1), instead of
+    // returning it, deliver it via exception.
+    if (result.second)
+    {
+        throw LLErrorEvent(desc, result.first);
+    }
+    // That way, our caller knows a simple return must be from the reply
+    // pump (pump 0).
+    return result.first;
+}
+
+LLSD errorLog(const LLEventWithID& result, const std::string& desc)
+{
+    // If the result arrived on the error pump (pump 1), log it as a fatal
+    // error.
+    if (result.second)
+    {
+        LL_ERRS("errorLog") << desc << ":" << std::endl;
+        LLSDSerialize::toPrettyXML(result.first, LL_CONT);
+        LL_CONT << LL_ENDL;
+    }
+    // A simple return must therefore be from the reply pump (pump 0).
+    return result.first;
+}
diff --git a/indra/llcommon/lleventcoro.h b/indra/llcommon/lleventcoro.h
new file mode 100644
index 0000000000000000000000000000000000000000..7232d1780f9142cf07f644bf6f36cb46851d9db8
--- /dev/null
+++ b/indra/llcommon/lleventcoro.h
@@ -0,0 +1,542 @@
+/**
+ * @file   lleventcoro.h
+ * @author Nat Goodspeed
+ * @date   2009-04-29
+ * @brief  Utilities to interface between coroutines and events.
+ * 
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * Copyright (c) 2009, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_LLEVENTCORO_H)
+#define LL_LLEVENTCORO_H
+
+#include <boost/coroutine/coroutine.hpp>
+#include <boost/coroutine/future.hpp>
+#include <boost/optional.hpp>
+#include <string>
+#include <stdexcept>
+#include "llevents.h"
+#include "llerror.h"
+
+/**
+ * Like LLListenerOrPumpName, this is a class intended for parameter lists:
+ * accept a <tt>const LLEventPumpOrPumpName&</tt> and you can accept either an
+ * <tt>LLEventPump&</tt> or its string name. For a single parameter that could
+ * be either, it's not hard to overload the function -- but as soon as you
+ * want to accept two such parameters, this is cheaper than four overloads.
+ */
+class LLEventPumpOrPumpName
+{
+public:
+    /// Pass an actual LLEventPump&
+    LLEventPumpOrPumpName(LLEventPump& pump):
+        mPump(pump)
+    {}
+    /// Pass the string name of an LLEventPump
+    LLEventPumpOrPumpName(const std::string& pumpname):
+        mPump(LLEventPumps::instance().obtain(pumpname))
+    {}
+    /// Pass string constant name of an LLEventPump. This override must be
+    /// explicit, since otherwise passing <tt>const char*</tt> to a function
+    /// accepting <tt>const LLEventPumpOrPumpName&</tt> would require two
+    /// different implicit conversions: <tt>const char*</tt> -> <tt>const
+    /// std::string&</tt> -> <tt>const LLEventPumpOrPumpName&</tt>.
+    LLEventPumpOrPumpName(const char* pumpname):
+        mPump(LLEventPumps::instance().obtain(pumpname))
+    {}
+    /// Unspecified: "I choose not to identify an LLEventPump."
+    LLEventPumpOrPumpName() {}
+    operator LLEventPump& () const { return *mPump; }
+    LLEventPump& getPump() const { return *mPump; }
+    operator bool() const { return mPump; }
+    bool operator!() const { return ! mPump; }
+
+private:
+    boost::optional<LLEventPump&> mPump;
+};
+
+/// This is an adapter for a signature like void LISTENER(const LLSD&), which
+/// isn't a valid LLEventPump listener: such listeners should return bool.
+template <typename LISTENER>
+class LLVoidListener
+{
+public:
+    LLVoidListener(const LISTENER& listener):
+        mListener(listener)
+    {}
+    bool operator()(const LLSD& event)
+    {
+        mListener(event);
+        // don't swallow the event, let other listeners see it
+        return false;
+    }
+private:
+    LISTENER mListener;
+};
+
+/// LLVoidListener helper function to infer the type of the LISTENER
+template <typename LISTENER>
+LLVoidListener<LISTENER> voidlistener(const LISTENER& listener)
+{
+    return LLVoidListener<LISTENER>(listener);
+}
+
+namespace LLEventDetail
+{
+    /**
+     * waitForEventOn() permits a coroutine to temporarily listen on an
+     * LLEventPump any number of times. We don't really want to have to ask
+     * the caller to label each such call with a distinct string; the whole
+     * point of waitForEventOn() is to present a nice sequential interface to
+     * the underlying LLEventPump-with-named-listeners machinery. So we'll use
+     * LLEventPump::inventName() to generate a distinct name for each
+     * temporary listener. On the other hand, because a given coroutine might
+     * call waitForEventOn() any number of times, we don't really want to
+     * consume an arbitrary number of generated inventName()s: that namespace,
+     * though large, is nonetheless finite. So we memoize an invented name for
+     * each distinct coroutine instance (each different 'self' object). We
+     * can't know the type of 'self', because it depends on the coroutine
+     * body's signature. So we cast its address to void*, looking for distinct
+     * pointer values. Yes, that means that an early coroutine could cache a
+     * value here, then be destroyed, only to be supplanted by a later
+     * coroutine (of the same or different type), and we'll end up
+     * "recognizing" the second one and reusing the listener name -- but
+     * that's okay, since it won't collide with any listener name used by the
+     * earlier coroutine since that earlier coroutine no longer exists.
+     */
+    std::string listenerNameForCoro(const void* self);
+
+    /**
+     * Implement behavior described for postAndWait()'s @a replyPumpNamePath
+     * parameter:
+     *
+     * * If <tt>path.isUndefined()</tt>, do nothing.
+     * * If <tt>path.isString()</tt>, @a dest is an LLSD map: store @a value
+     *   into <tt>dest[path.asString()]</tt>.
+     * * If <tt>path.isInteger()</tt>, @a dest is an LLSD array: store @a
+     *   value into <tt>dest[path.asInteger()]</tt>.
+     * * If <tt>path.isArray()</tt>, iteratively apply the rules above to step
+     *   down through the structure of @a dest. The last array entry in @a
+     *   path specifies the entry in the lowest-level structure in @a dest
+     *   into which to store @a value.
+     *
+     * @note
+     * In the degenerate case in which @a path is an empty array, @a dest will
+     * @em become @a value rather than @em containing it.
+     */
+    void storeToLLSDPath(LLSD& dest, const LLSD& path, const LLSD& value);
+} // namespace LLEventDetail
+
+/**
+ * Post specified LLSD event on the specified LLEventPump, then wait for a
+ * response on specified other LLEventPump. This is more than mere
+ * convenience: the difference between this function and the sequence
+ * @code
+ * requestPump.post(myEvent);
+ * LLSD reply = waitForEventOn(self, replyPump);
+ * @endcode
+ * is that the sequence above fails if the reply is posted immediately on
+ * @a replyPump, that is, before <tt>requestPump.post()</tt> returns. In the
+ * sequence above, the running coroutine isn't even listening on @a replyPump
+ * until <tt>requestPump.post()</tt> returns and @c waitForEventOn() is
+ * entered. Therefore, the coroutine completely misses an immediate reply
+ * event, making it wait indefinitely.
+ *
+ * By contrast, postAndWait() listens on the @a replyPump @em before posting
+ * the specified LLSD event on the specified @a requestPump.
+ *
+ * @param self The @c self object passed into a coroutine
+ * @param event LLSD data to be posted on @a requestPump
+ * @param requestPump an LLEventPump on which to post @a event. Pass either
+ * the LLEventPump& or its string name. However, if you pass a
+ * default-constructed @c LLEventPumpOrPumpName, we skip the post() call.
+ * @param replyPump an LLEventPump on which postAndWait() will listen for a
+ * reply. Pass either the LLEventPump& or its string name. The calling
+ * coroutine will wait until that reply arrives. (If you're concerned about a
+ * reply that might not arrive, please see also LLEventTimeout.)
+ * @param replyPumpNamePath specifies the location within @a event in which to
+ * store <tt>replyPump.getName()</tt>. This is a strictly optional convenience
+ * feature; obviously you can store the name in @a event "by hand" if desired.
+ * @a replyPumpNamePath can be specified in any of four forms:
+ * * @c isUndefined() (default-constructed LLSD object): do nothing. This is
+ *   the default behavior if you omit @a replyPumpNamePath.
+ * * @c isInteger(): @a event is an array. Store <tt>replyPump.getName()</tt>
+ *   in <tt>event[replyPumpNamePath.asInteger()]</tt>.
+ * * @c isString(): @a event is a map. Store <tt>replyPump.getName()</tt> in
+ *   <tt>event[replyPumpNamePath.asString()]</tt>.
+ * * @c isArray(): @a event has several levels of structure, e.g. map of
+ *   maps, array of arrays, array of maps, map of arrays, ... Store
+ *   <tt>replyPump.getName()</tt> in
+ *   <tt>event[replyPumpNamePath[0]][replyPumpNamePath[1]]...</tt> In other
+ *   words, examine each array entry in @a replyPumpNamePath in turn. If it's an
+ *   <tt>LLSD::String</tt>, the current level of @a event is a map; step down to
+ *   that map entry. If it's an <tt>LLSD::Integer</tt>, the current level of @a
+ *   event is an array; step down to that array entry. The last array entry in
+ *   @a replyPumpNamePath specifies the entry in the lowest-level structure in
+ *   @a event into which to store <tt>replyPump.getName()</tt>.
+ */
+template <typename SELF>
+LLSD postAndWait(SELF& self, const LLSD& event, const LLEventPumpOrPumpName& requestPump,
+                 const LLEventPumpOrPumpName& replyPump, const LLSD& replyPumpNamePath=LLSD())
+{
+    // declare the future
+    boost::coroutines::future<LLSD> future(self);
+    // make a callback that will assign a value to the future, and listen on
+    // the specified LLEventPump with that callback
+    std::string listenerName(LLEventDetail::listenerNameForCoro(&self));
+    LLTempBoundListener connection(
+        replyPump.getPump().listen(listenerName,
+                                   voidlistener(boost::coroutines::make_callback(future))));
+    // skip the "post" part if requestPump is default-constructed
+    if (requestPump)
+    {
+        // If replyPumpNamePath is non-empty, store the replyPump name in the
+        // request event.
+        LLSD modevent(event);
+        LLEventDetail::storeToLLSDPath(modevent, replyPumpNamePath, replyPump.getPump().getName());
+        LL_DEBUGS("lleventcoro") << "postAndWait(): coroutine " << listenerName
+                                 << " posting to " << requestPump.getPump().getName()
+                                 << ": " << modevent << LL_ENDL;
+        requestPump.getPump().post(modevent);
+    }
+    LL_DEBUGS("lleventcoro") << "postAndWait(): coroutine " << listenerName
+                             << " about to wait on LLEventPump " << replyPump.getPump().getName()
+                             << LL_ENDL;
+    // trying to dereference ("resolve") the future makes us wait for it
+    LLSD value(*future);
+    LL_DEBUGS("lleventcoro") << "postAndWait(): coroutine " << listenerName
+                             << " resuming with " << value << LL_ENDL;
+    // returning should disconnect the connection
+    return value;
+}
+
+/// Wait for the next event on the specified LLEventPump. Pass either the
+/// LLEventPump& or its string name.
+template <typename SELF>
+LLSD waitForEventOn(SELF& self, const LLEventPumpOrPumpName& pump)
+{
+    // This is now a convenience wrapper for postAndWait().
+    return postAndWait(self, LLSD(), LLEventPumpOrPumpName(), pump);
+}
+
+/// return type for two-pump variant of waitForEventOn()
+typedef std::pair<LLSD, int> LLEventWithID;
+
+namespace LLEventDetail
+{
+    /**
+     * This helper is specifically for the two-pump version of waitForEventOn().
+     * We use a single future object, but we want to listen on two pumps with it.
+     * Since we must still adapt from (the callable constructed by)
+     * boost::coroutines::make_callback() (void return) to provide an event
+     * listener (bool return), we've adapted LLVoidListener for the purpose. The
+     * basic idea is that we construct a distinct instance of WaitForEventOnHelper
+     * -- binding different instance data -- for each of the pumps. Then, when a
+     * pump delivers an LLSD value to either WaitForEventOnHelper, it can combine
+     * that LLSD with its discriminator to feed the future object.
+     */
+    template <typename LISTENER>
+    class WaitForEventOnHelper
+    {
+    public:
+        WaitForEventOnHelper(const LISTENER& listener, int discriminator):
+            mListener(listener),
+            mDiscrim(discriminator)
+        {}
+        // this signature is required for an LLEventPump listener
+        bool operator()(const LLSD& event)
+        {
+            // our future object is defined to accept LLEventWithID
+            mListener(LLEventWithID(event, mDiscrim));
+            // don't swallow the event, let other listeners see it
+            return false;
+        }
+    private:
+        LISTENER mListener;
+        const int mDiscrim;
+    };
+
+    /// WaitForEventOnHelper type-inference helper
+    template <typename LISTENER>
+    WaitForEventOnHelper<LISTENER> wfeoh(const LISTENER& listener, int discriminator)
+    {
+        return WaitForEventOnHelper<LISTENER>(listener, discriminator);
+    }
+} // namespace LLEventDetail
+
+/**
+ * This function waits for a reply on either of two specified LLEventPumps.
+ * Otherwise, it closely resembles postAndWait(); please see the documentation
+ * for that function for detailed parameter info.
+ *
+ * While we could have implemented the single-pump variant in terms of this
+ * one, there's enough added complexity here to make it worthwhile to give the
+ * single-pump variant its own straightforward implementation. Conversely,
+ * though we could use preprocessor logic to generate n-pump overloads up to
+ * BOOST_COROUTINE_WAIT_MAX, we don't foresee a use case. This two-pump
+ * overload exists because certain event APIs are defined in terms of a reply
+ * LLEventPump and an error LLEventPump.
+ *
+ * The LLEventWithID return value provides not only the received event, but
+ * the index of the pump on which it arrived (0 or 1).
+ *
+ * @note
+ * I'd have preferred to overload the name postAndWait() for both signatures.
+ * But consider the following ambiguous call:
+ * @code
+ * postAndWait(self, LLSD(), requestPump, replyPump, "someString");
+ * @endcode
+ * "someString" could be converted to either LLSD (@a replyPumpNamePath for
+ * the single-pump function) or LLEventOrPumpName (@a replyPump1 for two-pump
+ * function).
+ *
+ * It seems less burdensome to write postAndWait2() than to write either
+ * LLSD("someString") or LLEventOrPumpName("someString").
+ */
+template <typename SELF>
+LLEventWithID postAndWait2(SELF& self, const LLSD& event,
+                           const LLEventPumpOrPumpName& requestPump,
+                           const LLEventPumpOrPumpName& replyPump0,
+                           const LLEventPumpOrPumpName& replyPump1,
+                           const LLSD& replyPump0NamePath=LLSD(),
+                           const LLSD& replyPump1NamePath=LLSD())
+{
+    // declare the future
+    boost::coroutines::future<LLEventWithID> future(self);
+    // either callback will assign a value to this future; listen on
+    // each specified LLEventPump with a callback
+    std::string name(LLEventDetail::listenerNameForCoro(&self));
+    LLTempBoundListener connection0(
+        replyPump0.getPump().listen(name + "a",
+                               LLEventDetail::wfeoh(boost::coroutines::make_callback(future), 0)));
+    LLTempBoundListener connection1(
+        replyPump1.getPump().listen(name + "b",
+                               LLEventDetail::wfeoh(boost::coroutines::make_callback(future), 1)));
+    // skip the "post" part if requestPump is default-constructed
+    if (requestPump)
+    {
+        // If either replyPumpNamePath is non-empty, store the corresponding
+        // replyPump name in the request event.
+        LLSD modevent(event);
+        LLEventDetail::storeToLLSDPath(modevent, replyPump0NamePath,
+                                       replyPump0.getPump().getName());
+        LLEventDetail::storeToLLSDPath(modevent, replyPump1NamePath,
+                                       replyPump1.getPump().getName());
+        LL_DEBUGS("lleventcoro") << "postAndWait2(): coroutine " << name
+                                 << " posting to " << requestPump.getPump().getName()
+                                 << ": " << modevent << LL_ENDL;
+        requestPump.getPump().post(modevent);
+    }
+    LL_DEBUGS("lleventcoro") << "postAndWait2(): coroutine " << name
+                             << " about to wait on LLEventPumps " << replyPump0.getPump().getName()
+                             << ", " << replyPump1.getPump().getName() << LL_ENDL;
+    // trying to dereference ("resolve") the future makes us wait for it
+    LLEventWithID value(*future);
+    LL_DEBUGS("lleventcoro") << "postAndWait(): coroutine " << name
+                             << " resuming with (" << value.first << ", " << value.second << ")"
+                             << LL_ENDL;
+    // returning should disconnect both connections
+    return value;
+}
+
+/**
+ * Wait for the next event on either of two specified LLEventPumps.
+ */
+template <typename SELF>
+LLEventWithID
+waitForEventOn(SELF& self,
+               const LLEventPumpOrPumpName& pump0, const LLEventPumpOrPumpName& pump1)
+{
+    // This is now a convenience wrapper for postAndWait2().
+    return postAndWait2(self, LLSD(), LLEventPumpOrPumpName(), pump0, pump1);
+}
+
+/**
+ * Helper for the two-pump variant of waitForEventOn(), e.g.:
+ *
+ * @code
+ * LLSD reply = errorException(waitForEventOn(self, replyPump, errorPump),
+ *                             "error response from login.cgi");
+ * @endcode
+ *
+ * Examines an LLEventWithID, assuming that the second pump (pump 1) is
+ * listening for an error indication. If the incoming data arrived on pump 1,
+ * throw an LLErrorEvent exception. If the incoming data arrived on pump 0,
+ * just return it. Since a normal return can only be from pump 0, we no longer
+ * need the LLEventWithID's discriminator int; we can just return the LLSD.
+ *
+ * @note I'm not worried about introducing the (fairly generic) name
+ * errorException() into global namespace, because how many other overloads of
+ * the same name are going to accept an LLEventWithID parameter?
+ */
+LLSD errorException(const LLEventWithID& result, const std::string& desc);
+
+/**
+ * Exception thrown by errorException(). We don't call this LLEventError
+ * because it's not an error in event processing: rather, this exception
+ * announces an event that bears error information (for some other API).
+ */
+class LLErrorEvent: public std::runtime_error
+{
+public:
+    LLErrorEvent(const std::string& what, const LLSD& data):
+        std::runtime_error(what),
+        mData(data)
+    {}
+    virtual ~LLErrorEvent() throw() {}
+
+    LLSD getData() const { return mData; }
+
+private:
+    LLSD mData;
+};
+
+/**
+ * Like errorException(), save that this trips a fatal error using LL_ERRS
+ * rather than throwing an exception.
+ */
+LLSD errorLog(const LLEventWithID& result, const std::string& desc);
+
+/**
+ * Certain event APIs require the name of an LLEventPump on which they should
+ * post results. While it works to invent a distinct name and let
+ * LLEventPumps::obtain() instantiate the LLEventPump as a "named singleton,"
+ * in a certain sense it's more robust to instantiate a local LLEventPump and
+ * provide its name instead. This class packages the following idiom:
+ *
+ * 1. Instantiate a local LLCoroEventPump, with an optional name prefix.
+ * 2. Provide its actual name to the event API in question as the name of the
+ *    reply LLEventPump.
+ * 3. Initiate the request to the event API.
+ * 4. Call your LLEventTempStream's wait() method to wait for the reply.
+ * 5. Let the LLCoroEventPump go out of scope.
+ */
+class LLCoroEventPump
+{
+public:
+    LLCoroEventPump(const std::string& name="coro"):
+        mPump(name, true)           // allow tweaking the pump instance name
+    {}
+    /// It's typical to request the LLEventPump name to direct an event API to
+    /// send its response to this pump.
+    std::string getName() const { return mPump.getName(); }
+    /// Less typically, we'd request the pump itself for some reason.
+    LLEventPump& getPump() { return mPump; }
+
+    /**
+     * Wait for an event on this LLEventPump.
+     *
+     * @note
+     * The other major usage pattern we considered was to bind @c self at
+     * LLCoroEventPump construction time, which would avoid passing the
+     * parameter to each wait() call. But if we were going to bind @c self as
+     * a class member, we'd need to specify a class template parameter
+     * indicating its type. The big advantage of passing it to the wait() call
+     * is that the type can be implicit.
+     */
+    template <typename SELF>
+    LLSD wait(SELF& self)
+    {
+        return waitForEventOn(self, mPump);
+    }
+
+    template <typename SELF>
+    LLSD postAndWait(SELF& self, const LLSD& event, const LLEventPumpOrPumpName& requestPump,
+                     const LLSD& replyPumpNamePath=LLSD())
+    {
+        return ::postAndWait(self, event, requestPump, mPump, replyPumpNamePath);
+    }
+
+private:
+    LLEventStream mPump;
+};
+
+/**
+ * Other event APIs require the names of two different LLEventPumps: one for
+ * success response, the other for error response. Extend LLCoroEventPump
+ * for the two-pump use case.
+ */
+class LLCoroEventPumps
+{
+public:
+    LLCoroEventPumps(const std::string& name="coro",
+                     const std::string& suff0="Reply",
+                     const std::string& suff1="Error"):
+        mPump0(name + suff0, true),   // allow tweaking the pump instance name
+        mPump1(name + suff1, true)
+    {}
+    /// request pump 0's name
+    std::string getName0() const { return mPump0.getName(); }
+    /// request pump 1's name
+    std::string getName1() const { return mPump1.getName(); }
+    /// request both names
+    std::pair<std::string, std::string> getNames() const
+    {
+        return std::pair<std::string, std::string>(mPump0.getName(), mPump1.getName());
+    }
+
+    /// request pump 0
+    LLEventPump& getPump0() { return mPump0; }
+    /// request pump 1
+    LLEventPump& getPump1() { return mPump1; }
+
+    /// waitForEventOn(self, either of our two LLEventPumps)
+    template <typename SELF>
+    LLEventWithID wait(SELF& self)
+    {
+        return waitForEventOn(self, mPump0, mPump1);
+    }
+
+    /// errorException(wait(self))
+    template <typename SELF>
+    LLSD waitWithException(SELF& self)
+    {
+        return errorException(wait(self), std::string("Error event on ") + getName1());
+    }
+
+    /// errorLog(wait(self))
+    template <typename SELF>
+    LLSD waitWithLog(SELF& self)
+    {
+        return errorLog(wait(self), std::string("Error event on ") + getName1());
+    }
+
+    template <typename SELF>
+    LLEventWithID postAndWait(SELF& self, const LLSD& event,
+                              const LLEventPumpOrPumpName& requestPump,
+                              const LLSD& replyPump0NamePath=LLSD(),
+                              const LLSD& replyPump1NamePath=LLSD())
+    {
+        return postAndWait2(self, event, requestPump, mPump0, mPump1,
+                            replyPump0NamePath, replyPump1NamePath);
+    }
+
+    template <typename SELF>
+    LLSD postAndWaitWithException(SELF& self, const LLSD& event,
+                                  const LLEventPumpOrPumpName& requestPump,
+                                  const LLSD& replyPump0NamePath=LLSD(),
+                                  const LLSD& replyPump1NamePath=LLSD())
+    {
+        return errorException(postAndWait(self, event, requestPump,
+                                          replyPump0NamePath, replyPump1NamePath),
+                              std::string("Error event on ") + getName1());
+    }
+
+    template <typename SELF>
+    LLSD postAndWaitWithLog(SELF& self, const LLSD& event,
+                            const LLEventPumpOrPumpName& requestPump,
+                            const LLSD& replyPump0NamePath=LLSD(),
+                            const LLSD& replyPump1NamePath=LLSD())
+    {
+        return errorLog(postAndWait(self, event, requestPump,
+                                    replyPump0NamePath, replyPump1NamePath),
+                        std::string("Error event on ") + getName1());
+    }
+
+private:
+    LLEventStream mPump0, mPump1;
+};
+
+#endif /* ! defined(LL_LLEVENTCORO_H) */
diff --git a/indra/llcommon/lleventfilter.cpp b/indra/llcommon/lleventfilter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..74133781bef2503d05ba30f2a7e2509417d0c27f
--- /dev/null
+++ b/indra/llcommon/lleventfilter.cpp
@@ -0,0 +1,149 @@
+/**
+ * @file   lleventfilter.cpp
+ * @author Nat Goodspeed
+ * @date   2009-03-05
+ * @brief  Implementation for lleventfilter.
+ * 
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * Copyright (c) 2009, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "lleventfilter.h"
+// STL headers
+// std headers
+// external library headers
+#include <boost/bind.hpp>
+// other Linden headers
+#include "llerror.h"                // LL_ERRS
+#include "llsdutil.h"               // llsd_matches()
+
+LLEventFilter::LLEventFilter(LLEventPump& source, const std::string& name, bool tweak):
+    LLEventStream(name, tweak)
+{
+    source.listen(getName(), boost::bind(&LLEventFilter::post, this, _1));
+}
+
+LLEventMatching::LLEventMatching(const LLSD& pattern):
+    LLEventFilter("matching"),
+    mPattern(pattern)
+{
+}
+
+LLEventMatching::LLEventMatching(LLEventPump& source, const LLSD& pattern):
+    LLEventFilter(source, "matching"),
+    mPattern(pattern)
+{
+}
+
+bool LLEventMatching::post(const LLSD& event)
+{
+    if (! llsd_matches(mPattern, event).empty())
+        return false;
+
+    return LLEventStream::post(event);
+}
+
+LLEventTimeoutBase::LLEventTimeoutBase():
+    LLEventFilter("timeout")
+{
+}
+
+LLEventTimeoutBase::LLEventTimeoutBase(LLEventPump& source):
+    LLEventFilter(source, "timeout")
+{
+}
+
+void LLEventTimeoutBase::actionAfter(F32 seconds, const Action& action)
+{
+    setCountdown(seconds);
+    mAction = action;
+    if (! mMainloop.connected())
+    {
+        LLEventPump& mainloop(LLEventPumps::instance().obtain("mainloop"));
+        mMainloop = mainloop.listen(getName(), boost::bind(&LLEventTimeoutBase::tick, this, _1));
+    }
+}
+
+class ErrorAfter
+{
+public:
+    ErrorAfter(const std::string& message): mMessage(message) {}
+
+    void operator()()
+    {
+        LL_ERRS("LLEventTimeout") << mMessage << LL_ENDL;
+    }
+
+private:
+    std::string mMessage;
+};
+
+void LLEventTimeoutBase::errorAfter(F32 seconds, const std::string& message)
+{
+    actionAfter(seconds, ErrorAfter(message));
+}
+
+class EventAfter
+{
+public:
+    EventAfter(LLEventPump& pump, const LLSD& event):
+        mPump(pump),
+        mEvent(event)
+    {}
+
+    void operator()()
+    {
+        mPump.post(mEvent);
+    }
+
+private:
+    LLEventPump& mPump;
+    LLSD mEvent;
+};
+
+void LLEventTimeoutBase::eventAfter(F32 seconds, const LLSD& event)
+{
+    actionAfter(seconds, EventAfter(*this, event));
+}
+
+bool LLEventTimeoutBase::post(const LLSD& event)
+{
+    cancel();
+    return LLEventStream::post(event);
+}
+
+void LLEventTimeoutBase::cancel()
+{
+    mMainloop.disconnect();
+}
+
+bool LLEventTimeoutBase::tick(const LLSD&)
+{
+    if (countdownElapsed())
+    {
+        cancel();
+        mAction();
+    }
+    return false;                   // show event to other listeners
+}
+
+LLEventTimeout::LLEventTimeout() {}
+
+LLEventTimeout::LLEventTimeout(LLEventPump& source):
+    LLEventTimeoutBase(source)
+{
+}
+
+void LLEventTimeout::setCountdown(F32 seconds)
+{
+    mTimer.setTimerExpirySec(seconds);
+}
+
+bool LLEventTimeout::countdownElapsed() const
+{
+    return mTimer.hasExpired();
+}
diff --git a/indra/llcommon/lleventfilter.h b/indra/llcommon/lleventfilter.h
new file mode 100644
index 0000000000000000000000000000000000000000..fe1a631c6b57f08c3f3216a82a66d6333e37fea5
--- /dev/null
+++ b/indra/llcommon/lleventfilter.h
@@ -0,0 +1,186 @@
+/**
+ * @file   lleventfilter.h
+ * @author Nat Goodspeed
+ * @date   2009-03-05
+ * @brief  Define LLEventFilter: LLEventStream subclass with conditions
+ * 
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * Copyright (c) 2009, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_LLEVENTFILTER_H)
+#define LL_LLEVENTFILTER_H
+
+#include "llevents.h"
+#include "stdtypes.h"
+#include "lltimer.h"
+#include <boost/function.hpp>
+
+/**
+ * Generic base class
+ */
+class LLEventFilter: public LLEventStream
+{
+public:
+    /// construct a standalone LLEventFilter
+    LLEventFilter(const std::string& name="filter", bool tweak=true):
+        LLEventStream(name, tweak)
+    {}
+    /// construct LLEventFilter and connect it to the specified LLEventPump
+    LLEventFilter(LLEventPump& source, const std::string& name="filter", bool tweak=true);
+
+    /// Post an event to all listeners
+    virtual bool post(const LLSD& event) = 0;
+};
+
+/**
+ * Pass through only events matching a specified pattern
+ */
+class LLEventMatching: public LLEventFilter
+{
+public:
+    /// Pass an LLSD map with keys and values the incoming event must match
+    LLEventMatching(const LLSD& pattern);
+    /// instantiate and connect
+    LLEventMatching(LLEventPump& source, const LLSD& pattern);
+
+    /// Only pass through events matching the pattern
+    virtual bool post(const LLSD& event);
+
+private:
+    LLSD mPattern;
+};
+
+/**
+ * Wait for an event to be posted. If no such event arrives within a specified
+ * time, take a specified action. See LLEventTimeout for production
+ * implementation.
+ *
+ * @NOTE This is an abstract base class so that, for testing, we can use an
+ * alternate "timer" that doesn't actually consume real time.
+ */
+class LLEventTimeoutBase: public LLEventFilter
+{
+public:
+    /// construct standalone
+    LLEventTimeoutBase();
+    /// construct and connect
+    LLEventTimeoutBase(LLEventPump& source);
+
+    /// Callable, can be constructed with boost::bind()
+    typedef boost::function<void()> Action;
+
+    /**
+     * Start countdown timer for the specified number of @a seconds. Forward
+     * all events. If any event arrives before timer expires, cancel timer. If
+     * no event arrives before timer expires, take specified @a action.
+     *
+     * This is a one-shot timer. Once it has either expired or been canceled,
+     * it is inert until another call to actionAfter().
+     *
+     * Calling actionAfter() while an existing timer is running cheaply
+     * replaces that original timer. Thus, a valid use case is to detect
+     * idleness of some event source by calling actionAfter() on each new
+     * event. A rapid sequence of events will keep the timer from expiring;
+     * the first gap in events longer than the specified timer will fire the
+     * specified Action.
+     *
+     * Any post() call cancels the timer. To be satisfied with only a
+     * particular event, chain on an LLEventMatching that only passes such
+     * events:
+     *
+     * @code
+     * event                                                 ultimate
+     * source ---> LLEventMatching ---> LLEventTimeout  ---> listener
+     * @endcode
+     *
+     * @NOTE
+     * The implementation relies on frequent events on the LLEventPump named
+     * "mainloop".
+     */
+    void actionAfter(F32 seconds, const Action& action);
+
+    /**
+     * Like actionAfter(), but where the desired Action is LL_ERRS
+     * termination. Pass the timeout time and the desired LL_ERRS @a message.
+     *
+     * This method is useful when, for instance, some async API guarantees an
+     * event, whether success or failure, within a stated time window.
+     * Instantiate an LLEventTimeout listening to that API and call
+     * errorAfter() on each async request with a timeout comfortably longer
+     * than the API's time guarantee (much longer than the anticipated
+     * "mainloop" granularity).
+     *
+     * Then if the async API breaks its promise, the program terminates with
+     * the specified LL_ERRS @a message. The client of the async API can
+     * therefore assume the guarantee is upheld.
+     *
+     * @NOTE
+     * errorAfter() is implemented in terms of actionAfter(), so all remarks
+     * about calling actionAfter() also apply to errorAfter().
+     */
+    void errorAfter(F32 seconds, const std::string& message);
+
+    /**
+     * Like actionAfter(), but where the desired Action is a particular event
+     * for all listeners. Pass the timeout time and the desired @a event data.
+     * 
+     * Suppose the timeout should only be satisfied by a particular event, but
+     * the ultimate listener must see all other incoming events as well, plus
+     * the timeout @a event if any:
+     * 
+     * @code
+     * some        LLEventMatching                           LLEventMatching
+     * event  ---> for particular  ---> LLEventTimeout  ---> for timeout
+     * source      event                                     event \
+     *       \                                                      \ ultimate
+     *        `-----------------------------------------------------> listener
+     * @endcode
+     * 
+     * Since a given listener can listen on more than one LLEventPump, we can
+     * set things up so it sees the set union of events from LLEventTimeout
+     * and the original event source. However, as LLEventTimeout passes
+     * through all incoming events, the "particular event" that satisfies the
+     * left LLEventMatching would reach the ultimate listener twice. So we add
+     * an LLEventMatching that only passes timeout events.
+     *
+     * @NOTE
+     * eventAfter() is implemented in terms of actionAfter(), so all remarks
+     * about calling actionAfter() also apply to eventAfter().
+     */
+    void eventAfter(F32 seconds, const LLSD& event);
+
+    /// Pass event through, canceling the countdown timer
+    virtual bool post(const LLSD& event);
+
+    /// Cancel timer without event
+    void cancel();
+
+protected:
+    virtual void setCountdown(F32 seconds) = 0;
+    virtual bool countdownElapsed() const = 0;
+
+private:
+    bool tick(const LLSD&);
+
+    LLBoundListener mMainloop;
+    Action mAction;
+};
+
+/// Production implementation of LLEventTimoutBase
+class LLEventTimeout: public LLEventTimeoutBase
+{
+public:
+    LLEventTimeout();
+    LLEventTimeout(LLEventPump& source);
+
+protected:
+    virtual void setCountdown(F32 seconds);
+    virtual bool countdownElapsed() const;
+
+private:
+    LLTimer mTimer;
+};
+
+#endif /* ! defined(LL_LLEVENTFILTER_H) */
diff --git a/indra/llcommon/llevents.cpp b/indra/llcommon/llevents.cpp
index eb380ba7c8b04f02879fbb8bb3c95e01ed5801ea..7e3c6964dc4b15396c313766f56474455dd59bda 100644
--- a/indra/llcommon/llevents.cpp
+++ b/indra/llcommon/llevents.cpp
@@ -38,6 +38,7 @@
 #pragma warning (pop)
 #endif
 // other Linden headers
+#include "stringize.h"
 
 /*****************************************************************************
 *   queue_names: specify LLEventPump names that should be instantiated as
@@ -256,6 +257,12 @@ LLEventPump::~LLEventPump()
 // static data member
 const LLEventPump::NameList LLEventPump::empty;
 
+std::string LLEventPump::inventName(const std::string& pfx)
+{
+    static long suffix = 0;
+    return STRINGIZE(pfx << suffix++);
+}
+
 LLBoundListener LLEventPump::listen_impl(const std::string& name, const LLEventListener& listener,
                                          const NameList& after,
                                          const NameList& before)
diff --git a/indra/llcommon/llevents.h b/indra/llcommon/llevents.h
index 2f6515a4cb034f3d0b8ec8da5c085bab89536b51..20061f09c61b1c97b7f170b6e97aebcb80464134 100644
--- a/indra/llcommon/llevents.h
+++ b/indra/llcommon/llevents.h
@@ -19,7 +19,6 @@
 #include <map>
 #include <set>
 #include <vector>
-#include <list>
 #include <deque>
 #include <stdexcept>
 #include <boost/signals2.hpp>
@@ -28,13 +27,9 @@
 #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"
@@ -111,6 +106,9 @@ 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;
+/// Storing an LLBoundListener in LLTempBoundListener will disconnect the
+/// referenced listener when the LLTempBoundListener instance is destroyed.
+typedef boost::signals2::scoped_connection LLTempBoundListener;
 
 /**
  * A common idiom for event-based code is to accept either a callable --
@@ -254,14 +252,62 @@ namespace LLEventDetail
                                       const ConnectFunc& connect_func);
 } // namespace LLEventDetail
 
+/*****************************************************************************
+*   LLEventTrackable
+*****************************************************************************/
+/**
+ * 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;
+
 /*****************************************************************************
 *   LLEventPump
 *****************************************************************************/
 /**
  * LLEventPump is the base class interface through which we access the
  * concrete subclasses LLEventStream and LLEventQueue.
+ *
+ * @NOTE
+ * LLEventPump derives from LLEventTrackable so that when you "chain"
+ * LLEventPump instances together, they will automatically disconnect on
+ * destruction. Please see LLEventTrackable documentation for situations in
+ * which this may be perilous across threads.
  */
-class LLEventPump: boost::noncopyable
+class LLEventPump: public LLEventTrackable
 {
 public:
     /**
@@ -364,10 +410,22 @@ public:
      * 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.
+     * The listener name must be unique among active listeners for this
+     * LLEventPump, else you get DupListenerName. If you don't care to invent
+     * a name yourself, use inventName(). (I was tempted to recognize e.g. ""
+     * and internally generate a distinct name for that case. But that would
+     * handle badly the scenario in which you want to add, remove, re-add,
+     * etc. the same listener: each new listen() call would necessarily
+     * perform a new dependency sort. Assuming you specify the same
+     * after/before lists each time, using inventName() when you first
+     * instantiate your listener, then passing the same name on each listen()
+     * call, allows us to optimize away the second and subsequent dependency
+     * sorts.
+     *
+     * If (as is typical) you pass a <tt>boost::bind()</tt> expression as @a
+     * listener, 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
@@ -429,6 +487,9 @@ public:
     /// query
     virtual bool enabled() const { return mEnabled; }
 
+    /// Generate a distinct name for a listener -- see listen()
+    static std::string inventName(const std::string& pfx="listener");
+
 private:
     friend class LLEventPumps;
     /// flush queued events
@@ -503,47 +564,8 @@ private:
 };
 
 /*****************************************************************************
-*   LLEventTrackable and underpinnings
+*   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
diff --git a/indra/llcommon/llsdutil.cpp b/indra/llcommon/llsdutil.cpp
index 0202a033c3ccab7d5bd87f7d11701ad62f20d07c..643720cebe2ca543bda96cd022ebe24fba1eaa9c 100644
--- a/indra/llcommon/llsdutil.cpp
+++ b/indra/llcommon/llsdutil.cpp
@@ -46,6 +46,11 @@
 #endif
 
 #include "llsdserialize.h"
+#include "stringize.h"
+
+#include <map>
+#include <set>
+#include <boost/range.hpp>
 
 // U32
 LLSD ll_sd_from_U32(const U32 val)
@@ -313,3 +318,261 @@ BOOL compare_llsd_with_template(
 
 	return TRUE;
 }
+
+/*****************************************************************************
+*   Helpers for llsd_matches()
+*****************************************************************************/
+// raw data used for LLSD::Type lookup
+struct Data
+{
+    LLSD::Type type;
+    const char* name;
+} typedata[] =
+{
+#define def(type) { LLSD::type, #type + 4 }
+    def(TypeUndefined),
+    def(TypeBoolean),
+    def(TypeInteger),
+    def(TypeReal),
+    def(TypeString),
+    def(TypeUUID),
+    def(TypeDate),
+    def(TypeURI),
+    def(TypeBinary),
+    def(TypeMap),
+    def(TypeArray)
+#undef  def
+};
+
+// LLSD::Type lookup class into which we load the above static data
+class TypeLookup
+{
+    typedef std::map<LLSD::Type, std::string> MapType;
+
+public:
+    TypeLookup()
+    {
+        for (const Data *di(boost::begin(typedata)), *dend(boost::end(typedata)); di != dend; ++di)
+        {
+            mMap[di->type] = di->name;
+        }
+    }
+
+    std::string lookup(LLSD::Type type) const
+    {
+        MapType::const_iterator found = mMap.find(type);
+        if (found != mMap.end())
+        {
+            return found->second;
+        }
+        return STRINGIZE("<unknown LLSD type " << type << ">");
+    }
+
+private:
+    MapType mMap;
+};
+
+// static instance of the lookup class
+static const TypeLookup sTypes;
+
+// describe a mismatch; phrasing may want tweaking
+const std::string op(" required instead of ");
+
+// llsd_matches() wants to identify specifically where in a complex prototype
+// structure the mismatch occurred. This entails passing a prefix string,
+// empty for the top-level call. If the prototype contains an array of maps,
+// and the mismatch occurs in the second map in a key 'foo', we want to
+// decorate the returned string with: "[1]['foo']: etc." On the other hand, we
+// want to omit the entire prefix -- including colon -- if the mismatch is at
+// top level. This helper accepts the (possibly empty) recursively-accumulated
+// prefix string, returning either empty or the original string with colon
+// appended.
+static std::string colon(const std::string& pfx)
+{
+    if (pfx.empty())
+        return pfx;
+    return pfx + ": ";
+}
+
+// param type for match_types
+typedef std::vector<LLSD::Type> TypeVector;
+
+// The scalar cases in llsd_matches() use this helper. In most cases, we can
+// accept not only the exact type specified in the prototype, but also other
+// types convertible to the expected type. That implies looping over an array
+// of such types. If the actual type doesn't match any of them, we want to
+// provide a list of acceptable conversions as well as the exact type, e.g.:
+// "Integer (or Boolean, Real, String) required instead of UUID". Both the
+// implementation and the calling logic are simplified by separating out the
+// expected type from the convertible types.
+static std::string match_types(LLSD::Type expect, // prototype.type()
+                               const TypeVector& accept, // types convertible to that type
+                               LLSD::Type actual,        // type we're checking
+                               const std::string& pfx)   // as for llsd_matches
+{
+    // Trivial case: if the actual type is exactly what we expect, we're good.
+    if (actual == expect)
+        return "";
+
+    // For the rest of the logic, build up a suitable error string as we go so
+    // we only have to make a single pass over the list of acceptable types.
+    // If we detect success along the way, we'll simply discard the partial
+    // error string.
+    std::ostringstream out;
+    out << colon(pfx) << sTypes.lookup(expect);
+
+    // If there are any convertible types, append that list.
+    if (! accept.empty())
+    {
+        out << " (";
+        const char* sep = "or ";
+        for (TypeVector::const_iterator ai(accept.begin()), aend(accept.end());
+             ai != aend; ++ai, sep = ", ")
+        {
+            // Don't forget to return success if we match any of those types...
+            if (actual == *ai)
+                return "";
+            out << sep << sTypes.lookup(*ai);
+        }
+        out << ')';
+    }
+    // If we got this far, it's because 'actual' was not one of the acceptable
+    // types, so we must return an error. 'out' already contains colon(pfx)
+    // and the formatted list of acceptable types, so just append the mismatch
+    // phrase and the actual type.
+    out << op << sTypes.lookup(actual);
+    return out.str();
+}
+
+// see docstring in .h file
+std::string llsd_matches(const LLSD& prototype, const LLSD& data, const std::string& pfx)
+{
+    // An undefined prototype means that any data is valid.
+    // An undefined slot in an array or map prototype means that any data
+    // may fill that slot.
+    if (prototype.isUndefined())
+        return "";
+    // A prototype array must match a data array with at least as many
+    // entries. Moreover, every prototype entry must match the
+    // corresponding data entry.
+    if (prototype.isArray())
+    {
+        if (! data.isArray())
+        {
+            return STRINGIZE(colon(pfx) << "Array" << op << sTypes.lookup(data.type()));
+        }
+        if (data.size() < prototype.size())
+        {
+            return STRINGIZE(colon(pfx) << "Array size " << prototype.size() << op
+                             << "Array size " << data.size());
+        }
+        for (LLSD::Integer i = 0; i < prototype.size(); ++i)
+        {
+            std::string match(llsd_matches(prototype[i], data[i], STRINGIZE('[' << i << ']')));
+            if (! match.empty())
+            {
+                return match;
+            }
+        }
+        return "";
+    }
+    // A prototype map must match a data map. Every key in the prototype
+    // must have a corresponding key in the data map; every value in the
+    // prototype must match the corresponding key's value in the data.
+    if (prototype.isMap())
+    {
+        if (! data.isMap())
+        {
+            return STRINGIZE(colon(pfx) << "Map" << op << sTypes.lookup(data.type()));
+        }
+        // If there are a number of keys missing from the data, it would be
+        // frustrating to a coder to discover them one at a time, with a big
+        // build each time. Enumerate all missing keys.
+        std::ostringstream out;
+        out << colon(pfx);
+        const char* init = "Map missing keys: ";
+        const char* sep = init;
+        for (LLSD::map_const_iterator mi = prototype.beginMap(); mi != prototype.endMap(); ++mi)
+        {
+            if (! data.has(mi->first))
+            {
+                out << sep << mi->first;
+                sep = ", ";
+            }
+        }
+        // So... are we missing any keys?
+        if (sep != init)
+        {
+            return out.str();
+        }
+        // Good, the data block contains all the keys required by the
+        // prototype. Now match the prototype entries.
+        for (LLSD::map_const_iterator mi2 = prototype.beginMap(); mi2 != prototype.endMap(); ++mi2)
+        {
+            std::string match(llsd_matches(mi2->second, data[mi2->first],
+                                           STRINGIZE("['" << mi2->first << "']")));
+            if (! match.empty())
+            {
+                return match;
+            }
+        }
+        return "";
+    }
+    // A String prototype can match String, Boolean, Integer, Real, UUID,
+    // Date and URI, because any of these can be converted to String.
+    if (prototype.isString())
+    {
+        static LLSD::Type accept[] =
+        {
+            LLSD::TypeBoolean,
+            LLSD::TypeInteger,
+            LLSD::TypeReal,
+            LLSD::TypeUUID,
+            LLSD::TypeDate,
+            LLSD::TypeURI
+        };
+        return match_types(prototype.type(),
+                           TypeVector(boost::begin(accept), boost::end(accept)),
+                           data.type(),
+                           pfx);
+    }
+    // Boolean, Integer, Real match each other or String. TBD: ensure that
+    // a String value is numeric.
+    if (prototype.isBoolean() || prototype.isInteger() || prototype.isReal())
+    {
+        static LLSD::Type all[] =
+        {
+            LLSD::TypeBoolean,
+            LLSD::TypeInteger,
+            LLSD::TypeReal,
+            LLSD::TypeString
+        };
+        // Funny business: shuffle the set of acceptable types to include all
+        // but the prototype's type. Get the acceptable types in a set.
+        std::set<LLSD::Type> rest(boost::begin(all), boost::end(all));
+        // Remove the prototype's type because we pass that separately.
+        rest.erase(prototype.type());
+        return match_types(prototype.type(),
+                           TypeVector(rest.begin(), rest.end()),
+                           data.type(),
+                           pfx);
+    }
+    // UUID, Date and URI match themselves or String.
+    if (prototype.isUUID() || prototype.isDate() || prototype.isURI())
+    {
+        static LLSD::Type accept[] =
+        {
+            LLSD::TypeString
+        };
+        return match_types(prototype.type(),
+                           TypeVector(boost::begin(accept), boost::end(accept)),
+                           data.type(),
+                           pfx);
+    }
+    // We don't yet know the conversion semantics associated with any new LLSD
+    // data type that might be added, so until we've been extended to handle
+    // them, assume it's strict: the new type matches only itself. (This is
+    // true of Binary, which is why we don't handle that case separately.) Too
+    // bad LLSD doesn't define isConvertible(Type to, Type from).
+    return match_types(prototype.type(), TypeVector(), data.type(), pfx);
+}
diff --git a/indra/llcommon/llsdutil.h b/indra/llcommon/llsdutil.h
index 501600f1d906a367c14c8b520f7bafd8062730bd..0752f8aff1df0b025bd864dc834a6d237619b5d7 100644
--- a/indra/llcommon/llsdutil.h
+++ b/indra/llcommon/llsdutil.h
@@ -104,6 +104,61 @@ BOOL compare_llsd_with_template(
 	const LLSD& template_llsd,
 	LLSD& resultant_llsd);
 
+/**
+ * Recursively determine whether a given LLSD data block "matches" another
+ * LLSD prototype. The returned string is empty() on success, non-empty() on
+ * mismatch.
+ *
+ * This function tests structure (types) rather than data values. It is
+ * intended for when a consumer expects an LLSD block with a particular
+ * structure, and must succinctly detect whether the arriving block is
+ * well-formed. For instance, a test of the form:
+ * @code
+ * if (! (data.has("request") && data.has("target") && data.has("modifier") ...))
+ * @endcode
+ * could instead be expressed by initializing a prototype LLSD map with the
+ * required keys and writing:
+ * @code
+ * if (! llsd_matches(prototype, data).empty())
+ * @endcode
+ *
+ * A non-empty return value is an error-message fragment intended to indicate
+ * to (English-speaking) developers where in the prototype structure the
+ * mismatch occurred.
+ *
+ * * If a slot in the prototype isUndefined(), then anything is valid at that
+ *   place in the real object. (Passing prototype == LLSD() matches anything
+ *   at all.)
+ * * An array in the prototype must match a data array at least that large.
+ *   (Additional entries in the data array are ignored.) Every isDefined()
+ *   entry in the prototype array must match the corresponding entry in the
+ *   data array.
+ * * A map in the prototype must match a map in the data. Every key in the
+ *   prototype map must match a corresponding key in the data map. (Additional
+ *   keys in the data map are ignored.) Every isDefined() value in the
+ *   prototype map must match the corresponding key's value in the data map.
+ * * Scalar values in the prototype are tested for @em type rather than value.
+ *   For instance, a String in the prototype matches any String at all. In
+ *   effect, storing an Integer at a particular place in the prototype asserts
+ *   that the caller intends to apply asInteger() to the corresponding slot in
+ *   the data.
+ * * A String in the prototype matches String, Boolean, Integer, Real, UUID,
+ *   Date and URI, because asString() applied to any of these produces a
+ *   meaningful result.
+ * * Similarly, a Boolean, Integer or Real in the prototype can match any of
+ *   Boolean, Integer or Real in the data -- or even String.
+ * * UUID matches UUID or String.
+ * * Date matches Date or String.
+ * * URI matches URI or String.
+ * * Binary in the prototype matches only Binary in the data.
+ *
+ * @TODO: when a Boolean, Integer or Real in the prototype matches a String in
+ * the data, we should examine the String @em value to ensure it can be
+ * meaningfully converted to the requested type. The same goes for UUID, Date
+ * and URI.
+ */
+std::string llsd_matches(const LLSD& prototype, const LLSD& data, const std::string& pfx="");
+
 // Simple function to copy data out of input & output iterators if
 // there is no need for casting.
 template<typename Input> LLSD llsd_copy_array(Input iter, Input end)
diff --git a/indra/llcommon/tests/listener.h b/indra/llcommon/tests/listener.h
new file mode 100644
index 0000000000000000000000000000000000000000..fa12f944ef58def17b8c194b9a0e997b24540932
--- /dev/null
+++ b/indra/llcommon/tests/listener.h
@@ -0,0 +1,139 @@
+/**
+ * @file   listener.h
+ * @author Nat Goodspeed
+ * @date   2009-03-06
+ * @brief  Useful for tests of the LLEventPump family of classes
+ * 
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * Copyright (c) 2009, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_LISTENER_H)
+#define LL_LISTENER_H
+
+#include "llsd.h"
+#include <iostream>
+
+/*****************************************************************************
+*   test listener class
+*****************************************************************************/
+class Listener;
+std::ostream& operator<<(std::ostream&, const Listener&);
+
+/// Bear in mind that this is strictly for testing
+class Listener
+{
+public:
+    /// Every Listener is instantiated with a name
+    Listener(const std::string& name):
+        mName(name)
+    {
+//      std::cout << *this << ": ctor\n";
+    }
+/*==========================================================================*|
+    // These methods are only useful when trying to track Listener instance
+    // lifespan
+    Listener(const Listener& that):
+        mName(that.mName),
+        mLastEvent(that.mLastEvent)
+    {
+        std::cout << *this << ": copy\n";
+    }
+    virtual ~Listener()
+    {
+        std::cout << *this << ": dtor\n";
+    }
+|*==========================================================================*/
+    /// You can request the name
+    std::string getName() const { return mName; }
+    /// This is a typical listener method that returns 'false' when done,
+    /// allowing subsequent listeners on the LLEventPump to process the
+    /// incoming event.
+    bool call(const LLSD& event)
+    {
+//      std::cout << *this << "::call(" << event << ")\n";
+        mLastEvent = event;
+        return false;
+    }
+    /// This is an alternate listener that returns 'true' when done, which
+    /// stops processing of the incoming event.
+    bool callstop(const LLSD& event)
+    {
+//      std::cout << *this << "::callstop(" << event << ")\n";
+        mLastEvent = event;
+        return true;
+    }
+    /// ListenMethod can represent either call() or callstop().
+    typedef bool (Listener::*ListenMethod)(const LLSD&);
+    /**
+     * This helper method is only because our test code makes so many
+     * repetitive listen() calls to ListenerMethods. In real code, you should
+     * call LLEventPump::listen() directly so it can examine the specific
+     * object you pass to boost::bind().
+     */
+    LLBoundListener listenTo(LLEventPump& pump,
+                             ListenMethod method=&Listener::call,
+                             const LLEventPump::NameList& after=LLEventPump::empty,
+                             const LLEventPump::NameList& before=LLEventPump::empty)
+    {
+        return pump.listen(getName(), boost::bind(method, this, _1), after, before);
+    }
+    /// Both call() and callstop() set mLastEvent. Retrieve it.
+    LLSD getLastEvent() const
+    {
+//      std::cout << *this << "::getLastEvent() -> " << mLastEvent << "\n";
+        return mLastEvent;
+    }
+    /// Reset mLastEvent to a known state.
+    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;
+}
+
+/**
+ * This class tests the relative order in which various listeners on a given
+ * LLEventPump are called. Each listen() call binds a particular string, which
+ * we collect for later examination. The actual event is ignored.
+ */
+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;
+}
+
+#endif /* ! defined(LL_LISTENER_H) */
diff --git a/indra/llcommon/tests/lleventfilter_test.cpp b/indra/llcommon/tests/lleventfilter_test.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..28b909298e1979ccd9c554c5ba31592aebae4cdc
--- /dev/null
+++ b/indra/llcommon/tests/lleventfilter_test.cpp
@@ -0,0 +1,276 @@
+/**
+ * @file   lleventfilter_test.cpp
+ * @author Nat Goodspeed
+ * @date   2009-03-06
+ * @brief  Test for lleventfilter.
+ * 
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * Copyright (c) 2009, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "lleventfilter.h"
+// STL headers
+// std headers
+// external library headers
+// other Linden headers
+#include "../test/lltut.h"
+#include "stringize.h"
+#include "listener.h"
+#include "tests/wrapllerrs.h"
+
+/*****************************************************************************
+*   Test classes
+*****************************************************************************/
+// Strictly speaking, we're testing LLEventTimeoutBase rather than the
+// production LLEventTimeout (using LLTimer) because we don't want every test
+// run to pause for some number of seconds until we reach a real timeout. But
+// as we've carefully put all functionality except actual LLTimer calls into
+// LLEventTimeoutBase, that should suffice. We're not not not trying to test
+// LLTimer here.
+class TestEventTimeout: public LLEventTimeoutBase
+{
+public:
+    TestEventTimeout():
+        mElapsed(true)
+    {}
+    TestEventTimeout(LLEventPump& source):
+        LLEventTimeoutBase(source),
+        mElapsed(true)
+    {}
+
+    // test hook
+    void forceTimeout(bool timeout=true) { mElapsed = timeout; }
+
+protected:
+    virtual void setCountdown(F32 seconds) { mElapsed = false; }
+    virtual bool countdownElapsed() const { return mElapsed; }
+
+private:
+    bool mElapsed;
+};
+
+/*****************************************************************************
+*   TUT
+*****************************************************************************/
+namespace tut
+{
+    struct filter_data
+    {
+        // The resemblance between this test data and that in llevents_tut.cpp
+        // is not coincidental.
+        filter_data():
+            pumps(LLEventPumps::instance()),
+            mainloop(pumps.obtain("mainloop")),
+            listener0("first"),
+            listener1("second")
+        {}
+        LLEventPumps& pumps;
+        LLEventPump& mainloop;
+        Listener listener0;
+        Listener listener1;
+
+        void check_listener(const std::string& desc, const Listener& listener, const LLSD& got)
+        {
+            ensure_equals(STRINGIZE(listener << ' ' << desc),
+                          listener.getLastEvent(), got);
+        }
+    };
+    typedef test_group<filter_data> filter_group;
+    typedef filter_group::object filter_object;
+    filter_group filtergrp("lleventfilter");
+
+    template<> template<>
+    void filter_object::test<1>()
+    {
+        set_test_name("LLEventMatching");
+        LLEventPump& driver(pumps.obtain("driver"));
+        listener0.reset(0);
+        // Listener isn't derived from LLEventTrackable specifically to test
+        // various connection-management mechanisms. But that means we have a
+        // couple of transient Listener objects, one of which is listening to
+        // a persistent LLEventPump. Capture those connections in local
+        // LLTempBoundListener instances so they'll disconnect
+        // on destruction.
+        LLTempBoundListener temp1(
+            listener0.listenTo(driver));
+        // Construct a pattern LLSD: desired Event must have a key "foo"
+        // containing string "bar"
+        LLEventMatching filter(driver, LLSD().insert("foo", "bar"));
+        listener1.reset(0);
+        LLTempBoundListener temp2(
+            listener1.listenTo(filter));
+        driver.post(1);
+        check_listener("direct", listener0, LLSD(1));
+        check_listener("filtered", listener1, LLSD(0));
+        // Okay, construct an LLSD map matching the pattern
+        LLSD data;
+        data["foo"] = "bar";
+        data["random"] = 17;
+        driver.post(data);
+        check_listener("direct", listener0, data);
+        check_listener("filtered", listener1, data);
+    }
+
+    template<> template<>
+    void filter_object::test<2>()
+    {
+        set_test_name("LLEventTimeout::actionAfter()");
+        LLEventPump& driver(pumps.obtain("driver"));
+        TestEventTimeout filter(driver);
+        listener0.reset(0);
+        LLTempBoundListener temp1(
+            listener0.listenTo(filter));
+        // Use listener1.call() as the Action for actionAfter(), since it
+        // already provides a way to sense the call
+        listener1.reset(0);
+        // driver --> filter --> listener0
+        filter.actionAfter(20,
+                           boost::bind(&Listener::call, boost::ref(listener1), LLSD("timeout")));
+        // Okay, (fake) timer is ticking. 'filter' can only sense the timer
+        // when we pump mainloop. Do that right now to take the logic path
+        // before either the anticipated event arrives or the timer expires.
+        mainloop.post(17);
+        check_listener("no timeout 1", listener1, LLSD(0));
+        // Expected event arrives...
+        driver.post(1);
+        check_listener("event passed thru", listener0, LLSD(1));
+        // Should have canceled the timer. Verify that by asserting that the
+        // time has expired, then pumping mainloop again.
+        filter.forceTimeout();
+        mainloop.post(17);
+        check_listener("no timeout 2", listener1, LLSD(0));
+        // Verify chained actionAfter() calls, that is, that a second
+        // actionAfter() resets the timer established by the first
+        // actionAfter().
+        filter.actionAfter(20,
+                           boost::bind(&Listener::call, boost::ref(listener1), LLSD("timeout")));
+        // Since our TestEventTimeout class isn't actually manipulating time
+        // (quantities of seconds), only a bool "elapsed" flag, sense that by
+        // forcing the flag between actionAfter() calls.
+        filter.forceTimeout();
+        // Pumping mainloop here would result in a timeout (as we'll verify
+        // below). This state simulates a ticking timer that has not yet timed
+        // out. But now, before a mainloop event lets 'filter' recognize
+        // timeout on the previous actionAfter() call, pretend we're pushing
+        // that timeout farther into the future.
+        filter.actionAfter(20,
+                           boost::bind(&Listener::call, boost::ref(listener1), LLSD("timeout")));
+        // Look ma, no timeout!
+        mainloop.post(17);
+        check_listener("no timeout 3", listener1, LLSD(0));
+        // Now let the updated actionAfter() timer expire.
+        filter.forceTimeout();
+        // Notice the timeout.
+        mainloop.post(17);
+        check_listener("timeout", listener1, LLSD("timeout"));
+        // Timing out cancels the timer. Verify that.
+        listener1.reset(0);
+        filter.forceTimeout();
+        mainloop.post(17);
+        check_listener("no timeout 4", listener1, LLSD(0));
+        // Reset the timer and then cancel() it.
+        filter.actionAfter(20,
+                           boost::bind(&Listener::call, boost::ref(listener1), LLSD("timeout")));
+        // neither expired nor satisified
+        mainloop.post(17);
+        check_listener("no timeout 5", listener1, LLSD(0));
+        // cancel
+        filter.cancel();
+        // timeout!
+        filter.forceTimeout();
+        mainloop.post(17);
+        check_listener("no timeout 6", listener1, LLSD(0));
+    }
+
+    template<> template<>
+    void filter_object::test<3>()
+    {
+        set_test_name("LLEventTimeout::eventAfter()");
+        LLEventPump& driver(pumps.obtain("driver"));
+        TestEventTimeout filter(driver);
+        listener0.reset(0);
+        LLTempBoundListener temp1(
+            listener0.listenTo(filter));
+        filter.eventAfter(20, LLSD("timeout"));
+        // Okay, (fake) timer is ticking. 'filter' can only sense the timer
+        // when we pump mainloop. Do that right now to take the logic path
+        // before either the anticipated event arrives or the timer expires.
+        mainloop.post(17);
+        check_listener("no timeout 1", listener0, LLSD(0));
+        // Expected event arrives...
+        driver.post(1);
+        check_listener("event passed thru", listener0, LLSD(1));
+        // Should have canceled the timer. Verify that by asserting that the
+        // time has expired, then pumping mainloop again.
+        filter.forceTimeout();
+        mainloop.post(17);
+        check_listener("no timeout 2", listener0, LLSD(1));
+        // Set timer again.
+        filter.eventAfter(20, LLSD("timeout"));
+        // Now let the timer expire.
+        filter.forceTimeout();
+        // Notice the timeout.
+        mainloop.post(17);
+        check_listener("timeout", listener0, LLSD("timeout"));
+        // Timing out cancels the timer. Verify that.
+        listener0.reset(0);
+        filter.forceTimeout();
+        mainloop.post(17);
+        check_listener("no timeout 3", listener0, LLSD(0));
+    }
+
+    template<> template<>
+    void filter_object::test<4>()
+    {
+        set_test_name("LLEventTimeout::errorAfter()");
+        WrapLL_ERRS capture;
+        LLEventPump& driver(pumps.obtain("driver"));
+        TestEventTimeout filter(driver);
+        listener0.reset(0);
+        LLTempBoundListener temp1(
+            listener0.listenTo(filter));
+        filter.errorAfter(20, "timeout");
+        // Okay, (fake) timer is ticking. 'filter' can only sense the timer
+        // when we pump mainloop. Do that right now to take the logic path
+        // before either the anticipated event arrives or the timer expires.
+        mainloop.post(17);
+        check_listener("no timeout 1", listener0, LLSD(0));
+        // Expected event arrives...
+        driver.post(1);
+        check_listener("event passed thru", listener0, LLSD(1));
+        // Should have canceled the timer. Verify that by asserting that the
+        // time has expired, then pumping mainloop again.
+        filter.forceTimeout();
+        mainloop.post(17);
+        check_listener("no timeout 2", listener0, LLSD(1));
+        // Set timer again.
+        filter.errorAfter(20, "timeout");
+        // Now let the timer expire.
+        filter.forceTimeout();
+        // Notice the timeout.
+        std::string threw;
+        try
+        {
+            mainloop.post(17);
+        }
+        catch (const WrapLL_ERRS::FatalException& e)
+        {
+            threw = e.what();
+        }
+        ensure_contains("errorAfter() timeout exception", threw, "timeout");
+        // Timing out cancels the timer. Verify that.
+        listener0.reset(0);
+        filter.forceTimeout();
+        mainloop.post(17);
+        check_listener("no timeout 3", listener0, LLSD(0));
+    }
+} // namespace tut
+
+/*****************************************************************************
+*   Link dependencies
+*****************************************************************************/
+#include "llsdutil.cpp"
diff --git a/indra/llcommon/tests/wrapllerrs.h b/indra/llcommon/tests/wrapllerrs.h
new file mode 100644
index 0000000000000000000000000000000000000000..1001ebc466c8495db35b5f9b072d3f2f5b3af14a
--- /dev/null
+++ b/indra/llcommon/tests/wrapllerrs.h
@@ -0,0 +1,56 @@
+/**
+ * @file   wrapllerrs.h
+ * @author Nat Goodspeed
+ * @date   2009-03-11
+ * @brief  Define a class useful for unit tests that engage llerrs (LL_ERRS) functionality
+ * 
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * Copyright (c) 2009, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_WRAPLLERRS_H)
+#define LL_WRAPLLERRS_H
+
+#include "llerrorcontrol.h"
+
+struct WrapLL_ERRS
+{
+    WrapLL_ERRS():
+        // Resetting Settings discards the default Recorder that writes to
+        // stderr. Otherwise, expected llerrs (LL_ERRS) messages clutter the
+        // console output of successful tests, potentially confusing things.
+        mPriorErrorSettings(LLError::saveAndResetSettings()),
+        // Save shutdown function called by LL_ERRS
+        mPriorFatal(LLError::getFatalFunction())
+    {
+        // Make LL_ERRS call our own operator() method
+        LLError::setFatalFunction(boost::bind(&WrapLL_ERRS::operator(), this, _1));
+    }
+
+    ~WrapLL_ERRS()
+    {
+        LLError::setFatalFunction(mPriorFatal);
+        LLError::restoreSettings(mPriorErrorSettings);
+    }
+
+    struct FatalException: public std::runtime_error
+    {
+        FatalException(const std::string& what): std::runtime_error(what) {}
+    };
+
+    void operator()(const std::string& message)
+    {
+        // Save message for later in case consumer wants to sense the result directly
+        error = message;
+        // Also throw an appropriate exception since calling code is likely to
+        // assume that control won't continue beyond LL_ERRS.
+        throw FatalException(message);
+    }
+
+    std::string error;
+    LLError::Settings* mPriorErrorSettings;
+    LLError::FatalFunction mPriorFatal;
+};
+
+#endif /* ! defined(LL_WRAPLLERRS_H) */
diff --git a/indra/llmessage/CMakeLists.txt b/indra/llmessage/CMakeLists.txt
index c0f7a4d3354706dcacc048230bfb26c8b4348640..99bd98dfc1662a15c6317996b37008e420bb1f30 100644
--- a/indra/llmessage/CMakeLists.txt
+++ b/indra/llmessage/CMakeLists.txt
@@ -22,6 +22,7 @@ include_directories(
 
 set(llmessage_SOURCE_FILES
     llares.cpp
+    llareslistener.cpp
     llassetstorage.cpp
     llblowfishcipher.cpp
     llbuffer.cpp
@@ -104,6 +105,7 @@ set(llmessage_HEADER_FILES
     CMakeLists.txt
 
     llares.h
+    llareslistener.h
     llassetstorage.h
     llblowfishcipher.h
     llbuffer.h
@@ -222,4 +224,5 @@ IF (NOT LINUX AND VIEWER)
     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")
+    ADD_BUILD_TEST(llareslistener llmessage)
 ENDIF (NOT LINUX AND VIEWER)
diff --git a/indra/llmessage/llares.cpp b/indra/llmessage/llares.cpp
index fe37fe8142c1fd6102cda912f9787ee4dae642fc..acbf51d75ca26be890928c262ffd5e39f4f5918d 100644
--- a/indra/llmessage/llares.cpp
+++ b/indra/llmessage/llares.cpp
@@ -33,6 +33,7 @@
  */
 
 #include "linden_common.h"
+#include "llares.h"
 
 #include <ares_dns.h>
 #include <ares_version.h>
@@ -42,9 +43,10 @@
 #include "apr_poll.h"
 
 #include "llapr.h"
-#include "llares.h"
+#include "llareslistener.h"
 
 #if defined(LL_WINDOWS)
+#pragma warning (disable : 4355) // 'this' used in initializer list: yes, intentionally
 # define ns_c_in 1
 # define NS_HFIXEDSZ     12      /* #/bytes of fixed data in header */
 # define NS_QFIXEDSZ     4       /* #/bytes of fixed data in query */
@@ -102,7 +104,9 @@ void LLAres::QueryResponder::queryError(int code)
 }
 
 LLAres::LLAres() :
-chan_(NULL), mInitSuccess(false)
+    chan_(NULL),
+    mInitSuccess(false),
+    mListener(new LLAresListener("LLAres", this))
 {
 	if (ares_init(&chan_) != ARES_SUCCESS)
 	{
diff --git a/indra/llmessage/llares.h b/indra/llmessage/llares.h
index c709a08499473b7808909934236654c7df314357..78febcd560fdb2af07c4d053b1245fb1b0d810f8 100644
--- a/indra/llmessage/llares.h
+++ b/indra/llmessage/llares.h
@@ -36,7 +36,13 @@
 #define LL_LLARES_H
 
 #ifdef LL_WINDOWS
+// ares.h is broken on windows in that it depends on types defined in ws2tcpip.h
+// we need to include them first to work around it, but the headers issue warnings
+# pragma warning(push)
+# pragma warning(disable:4996)
+# include <winsock2.h>
 # include <ws2tcpip.h>
+# pragma warning(pop)
 #endif
 
 #ifdef LL_STANDALONE
@@ -49,7 +55,10 @@
 #include "llrefcount.h"
 #include "lluri.h"
 
+#include <boost/shared_ptr.hpp>
+
 class LLQueryResponder;
+class LLAresListener;
 
 /**
  * @brief Supported DNS RR types.
@@ -444,6 +453,9 @@ public:
 protected:
 	ares_channel chan_;
 	bool mInitSuccess;
+    // boost::scoped_ptr would actually fit the requirement better, but it
+    // can't handle incomplete types as boost::shared_ptr can.
+    boost::shared_ptr<LLAresListener> mListener;
 };
 	
 /**
diff --git a/indra/llmessage/llareslistener.cpp b/indra/llmessage/llareslistener.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8e1176cdd948ced26817b2390aad2d9bb3dbadd1
--- /dev/null
+++ b/indra/llmessage/llareslistener.cpp
@@ -0,0 +1,108 @@
+/**
+ * @file   llareslistener.cpp
+ * @author Nat Goodspeed
+ * @date   2009-03-18
+ * @brief  Implementation for llareslistener.
+ * 
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * Copyright (c) 2009, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if LL_WINDOWS
+#pragma warning (disable : 4355) // 'this' used in initializer list: yes, intentionally
+#endif
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "llareslistener.h"
+// STL headers
+// std headers
+// external library headers
+// other Linden headers
+#include "llares.h"
+#include "llerror.h"
+#include "llevents.h"
+
+LLAresListener::LLAresListener(const std::string& pumpname, LLAres* llares):
+    mAres(llares),
+    mBoundListener(LLEventPumps::instance().
+                   obtain(pumpname).
+                   listen("LLAresListener", boost::bind(&LLAresListener::process, this, _1)))
+{
+    mDispatch["rewriteURI"] = boost::bind(&LLAresListener::rewriteURI, this, _1);
+}
+
+bool LLAresListener::process(const LLSD& command)
+{
+    const std::string op(command["op"]);
+    // Look up the requested operation.
+    DispatchMap::const_iterator found = mDispatch.find(op);
+    if (found == mDispatch.end())
+    {
+        // There's no feedback other than our own reply. If somebody asks
+        // for an operation that's not supported (perhaps because of a
+        // typo?), unless we holler loudly, the request will be silently
+        // ignored. Throwing a tantrum on such errors will hopefully make
+        // this product more robust.
+        LL_ERRS("LLAresListener") << "Unsupported request " << op << LL_ENDL;
+        return false;
+    }
+    // Having found the operation, call it.
+    found->second(command);
+    // Conventional LLEventPump listener return
+    return false;
+}
+
+/// This UriRewriteResponder subclass packages returned URIs as an LLSD
+/// array to send back to the requester.
+class UriRewriteResponder: public LLAres::UriRewriteResponder
+{
+public:
+    /// Specify the event pump name on which to send the reply
+    UriRewriteResponder(const std::string& pumpname):
+        mPumpName(pumpname)
+    {}
+
+    /// Called by base class with results. This is called in both the
+    /// success and error cases. On error, the calling logic passes the
+    /// original URI.
+    virtual void rewriteResult(const std::vector<std::string>& uris)
+    {
+        LLSD result;
+        for (std::vector<std::string>::const_iterator ui(uris.begin()), uend(uris.end());
+             ui != uend; ++ui)
+        {
+            result.append(*ui);
+        }
+        LLEventPumps::instance().obtain(mPumpName).post(result);
+    }
+
+private:
+    const std::string mPumpName;
+};
+
+void LLAresListener::rewriteURI(const LLSD& data)
+{
+    const std::string uri(data["uri"]);
+    const std::string reply(data["reply"]);
+    // Validate that the request is well-formed
+    if (uri.empty() || reply.empty())
+    {
+        LL_ERRS("LLAresListener") << "rewriteURI request missing";
+        std::string separator;
+        if (uri.empty())
+        {
+            LL_CONT << " 'uri'";
+            separator = " and";
+        }
+        if (reply.empty())
+        {
+            LL_CONT << separator << " 'reply'";
+        }
+        LL_CONT << LL_ENDL;
+    }
+    // Looks as though we have what we need; issue the request
+    mAres->rewriteURI(uri, new UriRewriteResponder(reply));
+}
diff --git a/indra/llmessage/llareslistener.h b/indra/llmessage/llareslistener.h
new file mode 100644
index 0000000000000000000000000000000000000000..8835440c5d358367ec07695ef787a3970c27ddb1
--- /dev/null
+++ b/indra/llmessage/llareslistener.h
@@ -0,0 +1,47 @@
+/**
+ * @file   llareslistener.h
+ * @author Nat Goodspeed
+ * @date   2009-03-18
+ * @brief  LLEventPump API for LLAres. This header doesn't actually define the
+ *         API; the API is defined by the pump name on which this class
+ *         listens, and by the expected content of LLSD it receives.
+ * 
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * Copyright (c) 2009, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_LLARESLISTENER_H)
+#define LL_LLARESLISTENER_H
+
+#include <string>
+#include <map>
+#include <boost/function.hpp>
+#include "llevents.h"
+
+class LLAres;
+class LLSD;
+
+/// Listen on an LLEventPump with specified name for LLAres request events.
+class LLAresListener
+{
+public:
+    /// Specify the pump name on which to listen, and bind the LLAres instance
+    /// to use (e.g. gAres)
+    LLAresListener(const std::string& pumpname, LLAres* llares);
+
+    /// Handle request events on the event pump specified at construction time
+    bool process(const LLSD& command);
+
+private:
+    /// command["op"] == "rewriteURI" 
+    void rewriteURI(const LLSD& data);
+
+    typedef boost::function<void(const LLSD&)> Callable;
+    typedef std::map<std::string, Callable> DispatchMap;
+    DispatchMap mDispatch;
+    LLTempBoundListener mBoundListener;
+    LLAres* mAres;
+};
+
+#endif /* ! defined(LL_LLARESLISTENER_H) */
diff --git a/indra/llmessage/tests/llareslistener_test.cpp b/indra/llmessage/tests/llareslistener_test.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b8306d0fd9990e1a6b21976a0f98ae1be2af55ae
--- /dev/null
+++ b/indra/llmessage/tests/llareslistener_test.cpp
@@ -0,0 +1,194 @@
+/**
+ * @file   llareslistener_test.cpp
+ * @author Mark Palange
+ * @date   2009-02-26
+ * @brief  Tests of llareslistener.h.
+ * 
+ * $LicenseInfo:firstyear=2009&license=internal$
+ * Copyright (c) 2009, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if LL_WINDOWS
+#pragma warning (disable : 4355) // 'this' used in initializer list: yes, intentionally
+#endif
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "../llareslistener.h"
+// STL headers
+#include <iostream>
+// std headers
+// external library headers
+#include <boost/bind.hpp>
+
+// other Linden headers
+#include "llsd.h"
+#include "llares.h"
+#include "../test/lltut.h"
+#include "llevents.h"
+#include "tests/wrapllerrs.h"
+
+/*****************************************************************************
+*   Dummy stuff
+*****************************************************************************/
+LLAres::LLAres():
+    // Simulate this much of the real LLAres constructor: we need an
+    // LLAresListener instance.
+    mListener(new LLAresListener("LLAres", this))
+{}
+LLAres::~LLAres() {}
+void LLAres::rewriteURI(const std::string &uri,
+					LLAres::UriRewriteResponder *resp)
+{
+	// This is the only LLAres method I chose to implement.
+	// The effect is that LLAres returns immediately with
+	// a result that is equal to the input uri.
+	std::vector<std::string> result;
+	result.push_back(uri);
+	resp->rewriteResult(result);
+}
+
+LLAres::QueryResponder::~QueryResponder() {}
+void LLAres::QueryResponder::queryError(int) {}
+void LLAres::QueryResponder::queryResult(char const*, size_t) {}
+LLQueryResponder::LLQueryResponder() {}
+void LLQueryResponder::queryResult(char const*, size_t) {}
+void LLQueryResponder::querySuccess() {}
+void LLAres::UriRewriteResponder::queryError(int) {}
+void LLAres::UriRewriteResponder::querySuccess() {}
+void LLAres::UriRewriteResponder::rewriteResult(const std::vector<std::string>& uris) {}
+
+/*****************************************************************************
+*   TUT
+*****************************************************************************/
+namespace tut
+{
+    struct data
+    {
+        LLAres dummyAres;
+    };
+    typedef test_group<data> llareslistener_group;
+    typedef llareslistener_group::object object;
+    llareslistener_group llareslistenergrp("llareslistener");
+
+	struct ResponseCallback
+	{
+		std::vector<std::string> mURIs;
+		bool operator()(const LLSD& response)
+		{
+            mURIs.clear();
+            for (LLSD::array_const_iterator ri(response.beginArray()), rend(response.endArray());
+                 ri != rend; ++ri)
+            {
+                mURIs.push_back(*ri);
+            }
+            return false;
+		}
+	};
+
+    template<> template<>
+    void object::test<1>()
+    {
+        set_test_name("test event");
+		// Tests the success and failure cases, since they both use 
+		// the same code paths in the LLAres responder.
+		ResponseCallback response;
+        std::string pumpname("trigger");
+        // Since we're asking LLEventPumps to obtain() the pump by the desired
+        // name, it will persist beyond the current scope, so ensure we
+        // disconnect from it when 'response' goes away.
+        LLTempBoundListener temp(
+            LLEventPumps::instance().obtain(pumpname).listen("rewriteURIresponse",
+                                                             boost::bind(&ResponseCallback::operator(), &response, _1)));
+        // Now build an LLSD request that will direct its response events to
+        // that pump.
+		const std::string testURI("login.bar.com");
+        LLSD request;
+        request["op"] = "rewriteURI";
+        request["uri"] = testURI;
+        request["reply"] = pumpname;
+        LLEventPumps::instance().obtain("LLAres").post(request);
+		ensure_equals(response.mURIs.size(), 1);
+		ensure_equals(response.mURIs.front(), testURI); 
+	}
+
+    template<> template<>
+    void object::test<2>()
+    {
+        set_test_name("bad op");
+        WrapLL_ERRS capture;
+        LLSD request;
+        request["op"] = "foo";
+        std::string threw;
+        try
+        {
+            LLEventPumps::instance().obtain("LLAres").post(request);
+        }
+        catch (const WrapLL_ERRS::FatalException& e)
+        {
+            threw = e.what();
+        }
+        ensure_contains("LLAresListener bad op", threw, "Unsupported");
+    }
+
+    template<> template<>
+    void object::test<3>()
+    {
+        set_test_name("bad rewriteURI request");
+        WrapLL_ERRS capture;
+        LLSD request;
+        request["op"] = "rewriteURI";
+        std::string threw;
+        try
+        {
+            LLEventPumps::instance().obtain("LLAres").post(request);
+        }
+        catch (const WrapLL_ERRS::FatalException& e)
+        {
+            threw = e.what();
+        }
+        ensure_contains("LLAresListener bad op", threw, "missing 'uri' and 'reply'");
+    }
+
+    template<> template<>
+    void object::test<4>()
+    {
+        set_test_name("bad rewriteURI request");
+        WrapLL_ERRS capture;
+        LLSD request;
+        request["op"] = "rewriteURI";
+        request["reply"] = "nonexistent";
+        std::string threw;
+        try
+        {
+            LLEventPumps::instance().obtain("LLAres").post(request);
+        }
+        catch (const WrapLL_ERRS::FatalException& e)
+        {
+            threw = e.what();
+        }
+        ensure_contains("LLAresListener bad op", threw, "missing 'uri'");
+    }
+
+    template<> template<>
+    void object::test<5>()
+    {
+        set_test_name("bad rewriteURI request");
+        WrapLL_ERRS capture;
+        LLSD request;
+        request["op"] = "rewriteURI";
+        request["uri"] = "foo.bar.com";
+        std::string threw;
+        try
+        {
+            LLEventPumps::instance().obtain("LLAres").post(request);
+        }
+        catch (const WrapLL_ERRS::FatalException& e)
+        {
+            threw = e.what();
+        }
+        ensure_contains("LLAresListener bad op", threw, "missing 'reply'");
+    }
+}
diff --git a/indra/llmessage/tests/test_llsdmessage_peer.py b/indra/llmessage/tests/test_llsdmessage_peer.py
index e62f20912be58a8d7235ebb815a4141913624479..86d5761b1b4bf69c7ebcfccfb41814bdc97d89c9 100644
--- a/indra/llmessage/tests/test_llsdmessage_peer.py
+++ b/indra/llmessage/tests/test_llsdmessage_peer.py
@@ -16,16 +16,12 @@ import os
 import sys
 from threading import Thread
 from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
+
 mydir = os.path.dirname(__file__)       # expected to be .../indra/llmessage/tests/
 sys.path.insert(0, os.path.join(mydir, os.pardir, os.pardir, "lib", "python"))
 from indra.util.fastest_elementtree import parse as xml_parse
 from indra.base import llsd
-
-def debug(*args):
-    sys.stdout.writelines(args)
-    sys.stdout.flush()
-# comment out the line below to enable debug output
-debug = lambda *args: None
+from testrunner import run, debug
 
 class TestHTTPRequestHandler(BaseHTTPRequestHandler):
     """This subclass of BaseHTTPRequestHandler is to receive and echo
@@ -106,25 +102,5 @@ class TestHTTPServer(Thread):
         debug("Starting HTTP server...\n")
         httpd.serve_forever()
 
-def main(*args):
-    # Start HTTP server thread. Note that this and all other comm server
-    # threads should be daemon threads: we'll let them run "forever,"
-    # confident that the whole process will terminate when the main thread
-    # terminates, which will be when the test executable child process
-    # terminates.
-    httpThread = TestHTTPServer(name="httpd")
-    httpThread.setDaemon(True)
-    httpThread.start()
-    # choice of os.spawnv():
-    # - [v vs. l] pass a list of args vs. individual arguments,
-    # - [no p] don't use the PATH because we specifically want to invoke the
-    #   executable passed as our first arg,
-    # - [no e] child should inherit this process's environment.
-    debug("Running %s...\n" % (" ".join(args)))
-    sys.stdout.flush()
-    rc = os.spawnv(os.P_WAIT, args[0], args)
-    debug("%s returned %s\n" % (args[0], rc))
-    return rc
-
 if __name__ == "__main__":
-    sys.exit(main(*sys.argv[1:]))
+    sys.exit(run(server=TestHTTPServer(name="httpd"), *sys.argv[1:]))
diff --git a/indra/llmessage/tests/testrunner.py b/indra/llmessage/tests/testrunner.py
new file mode 100644
index 0000000000000000000000000000000000000000..3b9c3a7a1976dd8cba3a7e4daf8a1f21c6bfc558
--- /dev/null
+++ b/indra/llmessage/tests/testrunner.py
@@ -0,0 +1,53 @@
+#!/usr/bin/python
+"""\
+@file   testrunner.py
+@author Nat Goodspeed
+@date   2009-03-20
+@brief  Utilities for writing wrapper scripts for ADD_COMM_BUILD_TEST unit tests
+
+$LicenseInfo:firstyear=2009&license=viewergpl$
+Copyright (c) 2009, Linden Research, Inc.
+$/LicenseInfo$
+"""
+
+import os
+import sys
+
+def debug(*args):
+    sys.stdout.writelines(args)
+    sys.stdout.flush()
+# comment out the line below to enable debug output
+debug = lambda *args: None
+
+def run(*args, **kwds):
+    """All positional arguments collectively form a command line, executed as
+    a synchronous child process.
+    In addition, pass server=new_thread_instance as an explicit keyword (to
+    differentiate it from an additional command-line argument).
+    new_thread_instance should be an instantiated but not yet started Thread
+    subclass instance, e.g.:
+    run("python", "-c", 'print "Hello, world!"', server=TestHTTPServer(name="httpd"))
+    """
+    # If there's no server= keyword arg, don't start a server thread: simply
+    # run a child process.
+    try:
+        thread = kwds.pop("server")
+    except KeyError:
+        pass
+    else:
+        # Start server thread. Note that this and all other comm server
+        # threads should be daemon threads: we'll let them run "forever,"
+        # confident that the whole process will terminate when the main thread
+        # terminates, which will be when the child process terminates.
+        thread.setDaemon(True)
+        thread.start()
+    # choice of os.spawnv():
+    # - [v vs. l] pass a list of args vs. individual arguments,
+    # - [no p] don't use the PATH because we specifically want to invoke the
+    #   executable passed as our first arg,
+    # - [no e] child should inherit this process's environment.
+    debug("Running %s...\n" % (" ".join(args)))
+    sys.stdout.flush()
+    rc = os.spawnv(os.P_WAIT, args[0], args)
+    debug("%s returned %s\n" % (args[0], rc))
+    return rc
diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt
index e1f545adb55732ffd34fd5b74e3e9a0547c57602..5d79dfbc3e9f3573b1aa65495479f577241222b7 100644
--- a/indra/newview/CMakeLists.txt
+++ b/indra/newview/CMakeLists.txt
@@ -37,6 +37,7 @@ include(UI)
 include(UnixInstall)
 include(LLKDU)
 include(ViewerMiscLibs)
+include(LLLogin)
 
 if (WINDOWS)
     include(CopyWinLibs)
@@ -61,6 +62,7 @@ include_directories(
     ${LLXML_INCLUDE_DIRS}
     ${LSCRIPT_INCLUDE_DIRS}
     ${LSCRIPT_INCLUDE_DIRS}/lscript_compile
+    ${LLLOGIN_INCLUDE_DIRS}
     )
 
 set(viewer_SOURCE_FILES
@@ -231,6 +233,7 @@ set(viewer_SOURCE_FILES
     lllocationinputctrl.cpp
     lllogchat.cpp
     llloginhandler.cpp
+    lllogininstance.cpp
     llmanip.cpp
     llmaniprotate.cpp
     llmanipscale.cpp
@@ -315,7 +318,6 @@ set(viewer_SOURCE_FILES
     llslurl.cpp
     llspatialpartition.cpp
     llsprite.cpp
-    llsrv.cpp
     llstartup.cpp
     llstatusbar.cpp
     llstylemap.cpp
@@ -353,7 +355,6 @@ set(viewer_SOURCE_FILES
     llurlhistory.cpp
     llurlsimstring.cpp
     llurlwhitelist.cpp
-    lluserauth.cpp
     llvectorperfoptions.cpp
     llviewchildren.cpp
     llviewerassetstorage.cpp
@@ -432,6 +433,7 @@ set(viewer_SOURCE_FILES
     llworld.cpp
     llworldmap.cpp
     llworldmapview.cpp
+    llxmlrpclistener.cpp
     llxmlrpctransaction.cpp
     noise.cpp
     pipeline.cpp
@@ -627,6 +629,7 @@ set(viewer_HEADER_FILES
     lllocationinputctrl.h
     lllogchat.h
     llloginhandler.h
+    lllogininstance.h
     llmanip.h
     llmaniprotate.h
     llmanipscale.h
@@ -712,7 +715,6 @@ set(viewer_HEADER_FILES
     llslurl.h
     llspatialpartition.h
     llsprite.h
-    llsrv.h
     llstartup.h
     llstatusbar.h
     llstylemap.h
@@ -752,7 +754,6 @@ set(viewer_HEADER_FILES
     llurlhistory.h
     llurlsimstring.h
     llurlwhitelist.h
-    lluserauth.h
     llvectorperfoptions.h
     llviewchildren.h
     llviewerassetstorage.h
@@ -832,6 +833,7 @@ set(viewer_HEADER_FILES
     llworld.h
     llworldmap.h
     llworldmapview.h
+    llxmlrpclistener.h
     llxmlrpctransaction.h
     macmain.h
     noise.h
@@ -1266,6 +1268,7 @@ target_link_libraries(${VIEWER_BINARY_NAME}
     ${WINDOWS_LIBRARIES}
     ${XMLRPCEPI_LIBRARIES}
     ${ELFIO_LIBRARIES}
+    ${LLLOGIN_LIBRARIES}
     )
 
 build_version(viewer)
@@ -1390,3 +1393,5 @@ 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)
+ADD_VIEWER_COMM_BUILD_TEST(llxmlrpclistener viewer
+  ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_llxmlrpc_peer.py)
diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp
index 073b6b85fca0cbb8f70e206bdfebdf71521a40b3..455e987da0b3f81b377b5e95052d156db3303316 100644
--- a/indra/newview/llappviewer.cpp
+++ b/indra/newview/llappviewer.cpp
@@ -142,7 +142,6 @@
 #include "llfolderview.h"
 #include "lltoolbar.h"
 #include "llagentpilot.h"
-#include "llsrv.h"
 #include "llvovolume.h"
 #include "llflexibleobject.h" 
 #include "llvosurfacepatch.h"
@@ -204,9 +203,6 @@ BOOL gAllowTapTapHoldRun = TRUE;
 BOOL gShowObjectUpdates = FALSE;
 BOOL gUseQuickTime = TRUE;
 
-BOOL gAcceptTOS = FALSE;
-BOOL gAcceptCriticalMessage = FALSE;
-
 eLastExecEvent gLastExecEvent = LAST_EXEC_NORMAL;
 
 LLSD gDebugInfo;
diff --git a/indra/newview/llappviewer.h b/indra/newview/llappviewer.h
index 536abfae58be11e5fd96fc7a5a55b47781652abb..a7f1594d0e8c72728c885b9877b84e7a63b2d9d8 100644
--- a/indra/newview/llappviewer.h
+++ b/indra/newview/llappviewer.h
@@ -258,10 +258,6 @@ extern LLSD gDebugInfo;
 extern BOOL	gAllowTapTapHoldRun;
 extern BOOL	gShowObjectUpdates;
 
-extern BOOL gAcceptTOS;
-extern BOOL gAcceptCriticalMessage;
-
-
 typedef enum 
 {
 	LAST_EXEC_NORMAL = 0,
diff --git a/indra/newview/llclassifiedinfo.cpp b/indra/newview/llclassifiedinfo.cpp
index 5cf1579d0e0cc71616d80cadd6e93eb9545d1448..5fcafbeca6097461b41dbbcba25614fd140c1e22 100644
--- a/indra/newview/llclassifiedinfo.cpp
+++ b/indra/newview/llclassifiedinfo.cpp
@@ -38,35 +38,19 @@
 LLClassifiedInfo::cat_map LLClassifiedInfo::sCategories;
 
 // static
-void LLClassifiedInfo::loadCategories(LLUserAuth::options_t classified_options)
+void LLClassifiedInfo::loadCategories(const LLSD& options)
 {
-	LLUserAuth::options_t::iterator resp_it;
-	for (resp_it = classified_options.begin(); 
-		 resp_it != classified_options.end(); 
-		 ++resp_it)
+	for(LLSD::array_const_iterator resp_it = options.beginArray(),
+		end = options.endArray(); resp_it != end; ++resp_it)
 	{
-		const LLUserAuth::response_t& response = *resp_it;
-
-		LLUserAuth::response_t::const_iterator option_it;
-
-		S32 cat_id = 0;
-		option_it = response.find("category_id");
-		if (option_it != response.end())
+		LLSD name = (*resp_it)["category_name"];
+		if(name.isDefined())
 		{
-			cat_id = atoi(option_it->second.c_str());
+			LLSD id = (*resp_it)["category_id"];
+			if(id.isDefined())
+			{
+				LLClassifiedInfo::sCategories[id.asInteger()] = name.asString();
+			}
 		}
-		else
-		{
-			continue;
-		}
-
-		// Add the category id/name pair
-		option_it = response.find("category_name");
-		if (option_it != response.end())
-		{
-			LLClassifiedInfo::sCategories[cat_id] = option_it->second;
-		}
-
 	}
-
 }
diff --git a/indra/newview/llclassifiedinfo.h b/indra/newview/llclassifiedinfo.h
index cc5a6bf28fe341d4630c1afbaff91f3150060cca..37134c7e5bbd7cb7223f0d57c1c8b7f72bf05b9c 100644
--- a/indra/newview/llclassifiedinfo.h
+++ b/indra/newview/llclassifiedinfo.h
@@ -37,7 +37,6 @@
 
 #include "v3dmath.h"
 #include "lluuid.h"
-#include "lluserauth.h"
 
 class LLMessageSystem;
 
@@ -46,7 +45,7 @@ class LLClassifiedInfo
 public:
 	LLClassifiedInfo() {}
 
-	static void loadCategories(LLUserAuth::options_t event_options);
+	static void loadCategories(const LLSD& options);
 
 	typedef std::map<U32, std::string> cat_map;
 	static	cat_map sCategories;
diff --git a/indra/newview/lleventinfo.cpp b/indra/newview/lleventinfo.cpp
index d4175b6c846a9f3ee9919c32e1868bafa8d6e565..9be45d18fb0e833997386e87814340a4212384ae 100644
--- a/indra/newview/lleventinfo.cpp
+++ b/indra/newview/lleventinfo.cpp
@@ -87,35 +87,19 @@ void LLEventInfo::unpack(LLMessageSystem *msg)
 }
 
 // static
-void LLEventInfo::loadCategories(LLUserAuth::options_t event_options)
+void LLEventInfo::loadCategories(const LLSD& options)
 {
-	LLUserAuth::options_t::iterator resp_it;
-	for (resp_it = event_options.begin(); 
-		 resp_it != event_options.end(); 
-		 ++resp_it)
+	for(LLSD::array_const_iterator resp_it = options.beginArray(),
+		end = options.endArray(); resp_it != end; ++resp_it)
 	{
-		const LLUserAuth::response_t& response = *resp_it;
-
-		LLUserAuth::response_t::const_iterator option_it;
-
-		S32 cat_id = 0;
-		option_it = response.find("category_id");
-		if (option_it != response.end())
+		LLSD name = (*resp_it)["category_name"];
+		if(name.isDefined())
 		{
-			cat_id = atoi(option_it->second.c_str());
+			LLSD id = (*resp_it)["category_id"];
+			if(id.isDefined())
+			{
+				LLEventInfo::sCategories[id.asInteger()] = name.asString();
+			}
 		}
-		else
-		{
-			continue;
-		}
-
-		// Add the category id/name pair
-		option_it = response.find("category_name");
-		if (option_it != response.end())
-		{
-			LLEventInfo::sCategories[cat_id] = option_it->second;
-		}
-
 	}
-
 }
diff --git a/indra/newview/lleventinfo.h b/indra/newview/lleventinfo.h
index 880517a9f4b5f3cb3608348d19ea1d41b4da22ec..493c6599837bb97bdc5101b9686ac17fad1929d9 100644
--- a/indra/newview/lleventinfo.h
+++ b/indra/newview/lleventinfo.h
@@ -37,7 +37,6 @@
 
 #include "v3dmath.h"
 #include "lluuid.h"
-#include "lluserauth.h"
 
 class LLMessageSystem;
 
@@ -48,7 +47,7 @@ public:
 
 	void unpack(LLMessageSystem *msg);
 
-	static void loadCategories(LLUserAuth::options_t event_options);
+	static void loadCategories(const LLSD& options);
 
 public:
 	std::string mName;
diff --git a/indra/newview/lleventnotifier.cpp b/indra/newview/lleventnotifier.cpp
index c0fe327815af91f3e676150f5fb316bf4179bf5c..e54d78de2e366e822118941692157d70c5ebfc1c 100644
--- a/indra/newview/lleventnotifier.cpp
+++ b/indra/newview/lleventnotifier.cpp
@@ -95,18 +95,16 @@ void LLEventNotifier::update()
 	}
 }
 
-void LLEventNotifier::load(const LLUserAuth::options_t& event_options)
+void LLEventNotifier::load(const LLSD& event_options)
 {
-	LLUserAuth::options_t::const_iterator resp_it;
-	for (resp_it = event_options.begin(); 
-		 resp_it != event_options.end(); 
-		 ++resp_it)
+	for(LLSD::array_const_iterator resp_it = event_options.beginArray(),
+		end = event_options.endArray(); resp_it != end; ++resp_it)
 	{
-		const LLUserAuth::response_t& response = *resp_it;
+		LLSD response = *resp_it;
 
 		LLEventNotification *new_enp = new LLEventNotification();
 
-		if (!new_enp->load(response))
+		if(!new_enp->load(response))
 		{
 			delete new_enp;
 			continue;
@@ -207,49 +205,46 @@ bool LLEventNotification::handleResponse(const LLSD& notification, const LLSD& r
 	return false;
 }
 
-BOOL LLEventNotification::load(const LLUserAuth::response_t &response)
+BOOL LLEventNotification::load(const LLSD& response)
 {
-
-	LLUserAuth::response_t::const_iterator option_it;
 	BOOL event_ok = TRUE;
-	option_it = response.find("event_id");
-	if (option_it != response.end())
+	LLSD option = response.get("event_id");
+	if (option.isDefined())
 	{
-		mEventID = atoi(option_it->second.c_str());
+		mEventID = option.asInteger();
 	}
 	else
 	{
 		event_ok = FALSE;
 	}
 
-	option_it = response.find("event_name");
-	if (option_it != response.end())
+	option = response.get("event_name");
+	if (option.isDefined())
 	{
-		llinfos << "Event: " << option_it->second << llendl;
-		mEventName = option_it->second;
+		llinfos << "Event: " << option.asString() << llendl;
+		mEventName = option.asString();
 	}
 	else
 	{
 		event_ok = FALSE;
 	}
 
-
-	option_it = response.find("event_date");
-	if (option_it != response.end())
+	option = response.get("event_date");
+	if (option.isDefined())
 	{
-		llinfos << "EventDate: " << option_it->second << llendl;
-		mEventDateStr = option_it->second;
+		llinfos << "EventDate: " << option.asString() << llendl;
+		mEventDateStr = option.asString();
 	}
 	else
 	{
 		event_ok = FALSE;
 	}
 
-	option_it = response.find("event_date_ut");
-	if (option_it != response.end())
+	option = response.get("event_date_ut");
+	if (option.isDefined())
 	{
-		llinfos << "EventDate: " << option_it->second << llendl;
-		mEventDate = strtoul(option_it->second.c_str(), NULL, 10);
+		llinfos << "EventDate: " << option.asString() << llendl;
+		mEventDate = strtoul(option.asString().c_str(), NULL, 10);
 	}
 	else
 	{
@@ -261,44 +256,44 @@ BOOL LLEventNotification::load(const LLUserAuth::response_t &response)
 	S32 x_region = 0;
 	S32 y_region = 0;
 
-	option_it = response.find("grid_x");
-	if (option_it != response.end())
+	option = response.get("grid_x");
+	if (option.isDefined())
 	{
-		llinfos << "GridX: " << option_it->second << llendl;
-		grid_x= atoi(option_it->second.c_str());
+		llinfos << "GridX: " << option.asInteger() << llendl;
+		grid_x= option.asInteger();
 	}
 	else
 	{
 		event_ok = FALSE;
 	}
 
-	option_it = response.find("grid_y");
-	if (option_it != response.end())
+	option = response.get("grid_y");
+	if (option.isDefined())
 	{
-		llinfos << "GridY: " << option_it->second << llendl;
-		grid_y = atoi(option_it->second.c_str());
+		llinfos << "GridY: " << option.asInteger() << llendl;
+		grid_y = option.asInteger();
 	}
 	else
 	{
 		event_ok = FALSE;
 	}
 
-	option_it = response.find("x_region");
-	if (option_it != response.end())
+	option = response.get("x_region");
+	if (option.isDefined())
 	{
-		llinfos << "RegionX: " << option_it->second << llendl;
-		x_region = atoi(option_it->second.c_str());
+		llinfos << "RegionX: " << option.asInteger() << llendl;
+		x_region = option.asInteger();
 	}
 	else
 	{
 		event_ok = FALSE;
 	}
 
-	option_it = response.find("y_region");
-	if (option_it != response.end())
+	option = response.get("y_region");
+	if (option.isDefined())
 	{
-		llinfos << "RegionY: " << option_it->second << llendl;
-		y_region = atoi(option_it->second.c_str());
+		llinfos << "RegionY: " << option.asInteger() << llendl;
+		y_region = option.asInteger();
 	}
 	else
 	{
diff --git a/indra/newview/lleventnotifier.h b/indra/newview/lleventnotifier.h
index feb734948c2853428a3ecb608476199849c2a660..6fdde876465c3409fe5b1d6cc535ea3a1f2ab314 100644
--- a/indra/newview/lleventnotifier.h
+++ b/indra/newview/lleventnotifier.h
@@ -34,7 +34,6 @@
 #define LL_LLEVENTNOTIFIER_H
 
 #include "llframetimer.h"
-#include "lluserauth.h"
 #include "v3dmath.h"
 
 class LLEventInfo;
@@ -49,7 +48,7 @@ public:
 
 	void update();	// Notify the user of the event if it's coming up
 
-	void load(const LLUserAuth::options_t& event_options);	// In the format that it comes in from LLUserAuth
+	void load(const LLSD& event_options);	// In the format that it comes in from login
 	void add(LLEventInfo &event_info);	// Add a new notification for an event
 	void remove(U32 event_id);
 
@@ -69,7 +68,7 @@ public:
 	LLEventNotification();
 	virtual ~LLEventNotification();
 
-	BOOL load(const LLUserAuth::response_t &en);		// In the format it comes in from LLUserAuth
+	BOOL load(const LLSD& en);		// In the format it comes in from login
 	BOOL load(const LLEventInfo &event_info);		// From existing event_info on the viewer.
 	//void setEventID(const U32 event_id);
 	//void setEventName(std::string &event_name);
diff --git a/indra/newview/llfloatertos.cpp b/indra/newview/llfloatertos.cpp
index 764a6a3498f56c93b164ef0f736b9eacd5589a52..c79e96a5e5a1f73a0af5363686224813712a61f4 100644
--- a/indra/newview/llfloatertos.cpp
+++ b/indra/newview/llfloatertos.cpp
@@ -36,8 +36,6 @@
 
 // viewer includes
 #include "llagent.h"
-#include "llappviewer.h"
-#include "llstartup.h"
 #include "llviewerstats.h"
 #include "llviewertexteditor.h"
 #include "llviewerwindow.h"
@@ -58,11 +56,13 @@
 LLFloaterTOS* LLFloaterTOS::sInstance = NULL;
 
 // static
-LLFloaterTOS* LLFloaterTOS::show(ETOSType type, const std::string & message)
+LLFloaterTOS* LLFloaterTOS::show(ETOSType type, 
+								 const std::string & message, 
+								 const YesNoCallback& callback)
 {
 	if( !LLFloaterTOS::sInstance )
 	{
-		LLFloaterTOS::sInstance = new LLFloaterTOS(type, message);
+		LLFloaterTOS::sInstance = new LLFloaterTOS(type, message, callback);
 	}
 
 	if (type == TOS_TOS)
@@ -78,12 +78,15 @@ LLFloaterTOS* LLFloaterTOS::show(ETOSType type, const std::string & message)
 }
 
 
-LLFloaterTOS::LLFloaterTOS(ETOSType type, const std::string & message)
+LLFloaterTOS::LLFloaterTOS(ETOSType type, 
+						   const std::string & message, 
+						   const YesNoCallback& callback)
 :	LLModalDialog( std::string(" "), 100, 100 ),
 	mType(type),
 	mMessage(message),
 	mWebBrowserWindowId( 0 ),
-	mLoadCompleteCount( 0 )
+	mLoadCompleteCount( 0 ),
+	mCallback(callback)
 {
 }
 
@@ -235,25 +238,12 @@ void LLFloaterTOS::onContinue( void* userdata )
 {
 	LLFloaterTOS* self = (LLFloaterTOS*) userdata;
 	llinfos << "User agrees with TOS." << llendl;
-	if (self->mType == TOS_TOS)
-	{
-		gAcceptTOS = TRUE;
-	}
-	else
-	{
-		gAcceptCriticalMessage = TRUE;
-	}
 
-	// Testing TOS dialog
-	#if ! LL_RELEASE_FOR_DOWNLOAD		
-	if ( LLStartUp::getStartupState() == STATE_LOGIN_WAIT )
+	if(self->mCallback)
 	{
-		LLStartUp::setStartupState( STATE_LOGIN_SHOW );
+		self->mCallback(true);
 	}
-	else 
-	#endif
 
-	LLStartUp::setStartupState( STATE_LOGIN_AUTH_INIT );			// Go back and finish authentication
 	self->closeFloater(); // destroys this object
 }
 
@@ -262,8 +252,12 @@ void LLFloaterTOS::onCancel( void* userdata )
 {
 	LLFloaterTOS* self = (LLFloaterTOS*) userdata;
 	llinfos << "User disagrees with TOS." << llendl;
-	LLNotifications::instance().add("MustAgreeToLogIn", LLSD(), LLSD(), login_alert_done);
-	LLStartUp::setStartupState( STATE_LOGIN_SHOW );
+
+	if(self->mCallback)
+	{
+		self->mCallback(false);
+	}
+
 	self->mLoadCompleteCount = 0;  // reset counter for next time we come to TOS
 	self->closeFloater(); // destroys this object
 }
diff --git a/indra/newview/llfloatertos.h b/indra/newview/llfloatertos.h
index dbec3ff8b613edda945f417fa23d0802b88252a6..67d2f0ceecb9e797a8bcdea89b2c20c063e82067 100644
--- a/indra/newview/llfloatertos.h
+++ b/indra/newview/llfloatertos.h
@@ -36,6 +36,7 @@
 #include "llmodaldialog.h"
 #include "llassetstorage.h"
 #include "llwebbrowserctrl.h"
+#include <boost/function.hpp>
 
 class LLButton;
 class LLRadioGroup;
@@ -57,8 +58,12 @@ public:
 		TOS_CRITICAL_MESSAGE = 1
 	};
 
+	typedef boost::function<void(bool)> YesNoCallback;
+
 	// Asset_id is overwritten with LLUUID::null when agree is clicked.
-	static LLFloaterTOS* show(ETOSType type, const std::string & message);
+	static LLFloaterTOS* show(ETOSType type, 
+							  const std::string & message, 
+							  const YesNoCallback& callback);
 
 	BOOL postBuild();
 	
@@ -74,13 +79,16 @@ public:
 
 private:
 	// Asset_id is overwritten with LLUUID::null when agree is clicked.
-	LLFloaterTOS(ETOSType type, const std::string & message);
+	LLFloaterTOS(ETOSType type, 
+				 const std::string & message, 
+				 const YesNoCallback& callback);
 
 private:
 	ETOSType		mType;
 	std::string		mMessage;
 	int				mWebBrowserWindowId;
 	int				mLoadCompleteCount;
+	YesNoCallback	mCallback;
 
 	static LLFloaterTOS* sInstance;
 };
diff --git a/indra/newview/llinventorymodel.cpp b/indra/newview/llinventorymodel.cpp
index 1176bf87351441f7f37b548d90636b165f6d072a..4e2bb3e2e9f449af7c4f550313fd7c753c7fb55b 100644
--- a/indra/newview/llinventorymodel.cpp
+++ b/indra/newview/llinventorymodel.cpp
@@ -1876,63 +1876,56 @@ bool LLInventoryModel::isCategoryComplete(const LLUUID& cat_id) const
 }
 
 bool LLInventoryModel::loadSkeleton(
-	const LLInventoryModel::options_t& options,
+	const LLSD& options,
 	const LLUUID& owner_id)
 {
 	lldebugs << "importing inventory skeleton for " << owner_id << llendl;
 
 	typedef std::set<LLPointer<LLViewerInventoryCategory>, InventoryIDPtrLess> cat_set_t;
 	cat_set_t temp_cats;
+	bool rv = true;
 
-	update_map_t child_counts;
+	for(LLSD::array_const_iterator it = options.beginArray(),
+		end = options.endArray(); it != end; ++it)
+	{
+		LLSD name = (*it)["name"];
+		LLSD folder_id = (*it)["folder_id"];
+		LLSD parent_id = (*it)["parent_id"];
+		LLSD version = (*it)["version"];
+		if(name.isDefined()
+			&& folder_id.isDefined()
+			&& parent_id.isDefined()
+			&& version.isDefined()
+			&& folder_id.asUUID().notNull() // if an id is null, it locks the viewer.
+			) 		
+		{
+			LLPointer<LLViewerInventoryCategory> cat = new LLViewerInventoryCategory(owner_id);
+			cat->rename(name.asString());
+			cat->setUUID(folder_id.asUUID());
+			cat->setParent(parent_id.asUUID());
 
-	LLUUID id;
-	LLAssetType::EType preferred_type;
-	bool rv = true;
-	for(options_t::const_iterator it = options.begin(); it < options.end(); ++it)
-	{
-		LLPointer<LLViewerInventoryCategory> cat = new LLViewerInventoryCategory(owner_id);
-		response_t::const_iterator no_response = (*it).end();
-		response_t::const_iterator skel;
-		skel = (*it).find("name");
-		if(skel == no_response) goto clean_cat;
-		cat->rename(std::string((*skel).second));
-		skel = (*it).find("folder_id");
-		if(skel == no_response) goto clean_cat;
-		id.set((*skel).second);
-		// if an id is null, it locks the viewer.
-		if(id.isNull()) goto clean_cat;
-		cat->setUUID(id);
-		skel = (*it).find("parent_id");
-		if(skel == no_response) goto clean_cat;
-		id.set((*skel).second);
-		cat->setParent(id);
-		skel = (*it).find("type_default");
-		if(skel == no_response)
-		{
-			preferred_type = LLAssetType::AT_NONE;
+			LLAssetType::EType preferred_type = LLAssetType::AT_NONE;
+			LLSD type_default = (*it)["type_default"];
+			if(type_default.isDefined())
+			{
+				preferred_type = (LLAssetType::EType)type_default.asInteger();
+			}
+			cat->setPreferredType(preferred_type);
+			cat->setVersion(version.asInteger());
+			temp_cats.insert(cat);
 		}
 		else
 		{
-			S32 t = atoi((*skel).second.c_str());
-			preferred_type = (LLAssetType::EType)t;
+			llwarns << "Unable to import near " << name.asString() << llendl;
+			rv = false;
 		}
-		cat->setPreferredType(preferred_type);
-		skel = (*it).find("version");
-		if(skel == no_response) goto clean_cat;
-		cat->setVersion(atoi((*skel).second.c_str()));
-		temp_cats.insert(cat);
-		continue;
-	clean_cat:
-		llwarns << "Unable to import near " << cat->getName() << llendl;
-		rv = false;
-		//delete cat; // automatic when cat is reasigned or destroyed
 	}
 
 	S32 cached_category_count = 0;
 	S32 cached_item_count = 0;
 	if(!temp_cats.empty())
 	{
+		update_map_t child_counts;
 		cat_array_t categories;
 		item_array_t items;
 		std::string owner_id_str;
@@ -1961,6 +1954,7 @@ bool LLInventoryModel::loadSkeleton(
 				llinfos << "Unable to gunzip " << gzip_filename << llendl;
 			}
 		}
+
 		if(loadFromFile(inventory_filename, categories, items))
 		{
 			// We were able to find a cache of files. So, use what we
@@ -2085,85 +2079,84 @@ bool LLInventoryModel::loadSkeleton(
 	return rv;
 }
 
-bool LLInventoryModel::loadMeat(
-	const LLInventoryModel::options_t& options, const LLUUID& owner_id)
+bool LLInventoryModel::loadMeat(const LLSD& options, const LLUUID& owner_id)
 {
 	llinfos << "importing inventory for " << owner_id << llendl;
-	LLPermissions default_perm;
-	default_perm.init(LLUUID::null, owner_id, LLUUID::null, LLUUID::null);
-	LLPointer<LLViewerInventoryItem> item;
-	LLUUID id;
-	LLAssetType::EType type;
-	LLInventoryType::EType inv_type;
 	bool rv = true;
-	for(options_t::const_iterator it = options.begin(); it < options.end(); ++it)
-	{
-		item = new LLViewerInventoryItem;
-		response_t::const_iterator no_response = (*it).end();
-		response_t::const_iterator meat;
-		meat = (*it).find("name");
-		if(meat == no_response) goto clean_item;
-		item->rename(std::string((*meat).second));
-		meat = (*it).find("item_id");
-		if(meat == no_response) goto clean_item;
-		id.set((*meat).second);
-		item->setUUID(id);
-		meat = (*it).find("parent_id");
-		if(meat == no_response) goto clean_item;
-		id.set((*meat).second);
-		item->setParent(id);
-		meat = (*it).find("type");
-		if(meat == no_response) goto clean_item;
-		type = (LLAssetType::EType)atoi((*meat).second.c_str());
-		item->setType(type);
-		meat = (*it).find("inv_type");
-		if(meat != no_response)
-		{
-			inv_type = (LLInventoryType::EType)atoi((*meat).second.c_str());
-			item->setInventoryType(inv_type);
-		}
-		meat = (*it).find("data_id");
-		if(meat == no_response) goto clean_item;
-		id.set((*meat).second);
-		if(LLAssetType::AT_CALLINGCARD == type)
-		{
-			LLPermissions perm;
-			perm.init(id, owner_id, LLUUID::null, LLUUID::null);
-			item->setPermissions(perm);
-		}
-		else
+	for(LLSD::array_const_iterator it = options.beginArray(),
+		end = options.endArray(); it != end; ++it)
+	{
+		LLSD name = (*it)["name"];
+		LLSD item_id = (*it)["item_id"];
+		LLSD parent_id = (*it)["parent_id"];
+		LLSD asset_type = (*it)["type"];
+		LLSD data_id = (*it)["data_id"];
+		if(name.isDefined() 
+			&& item_id.isDefined()
+			&& parent_id.isDefined()
+			&& asset_type.isDefined()
+			&& data_id.isDefined())
 		{
-			meat = (*it).find("perm_mask");
-			if(meat != no_response)
+			LLPointer<LLViewerInventoryItem> item = new LLViewerInventoryItem;
+			item->rename(name.asString());
+			item->setUUID(item_id.asUUID());
+			item->setParent(parent_id.asUUID());
+			LLAssetType::EType type = (LLAssetType::EType)asset_type.asInteger();
+			item->setType(type);
+
+			LLSD llsd_inv_type = (*it)["inv_type"];
+			if(llsd_inv_type.isDefined())
 			{
-				PermissionMask perm_mask = atoi((*meat).second.c_str());
-				default_perm.initMasks(
-					perm_mask, perm_mask, perm_mask, perm_mask, perm_mask);
+				LLInventoryType::EType inv_type = (LLInventoryType::EType)llsd_inv_type.asInteger();
+				item->setInventoryType(inv_type);
+			}
+
+			if(LLAssetType::AT_CALLINGCARD == type)
+			{
+				LLPermissions perm;
+				perm.init(data_id.asUUID(), owner_id, LLUUID::null, LLUUID::null);
+				item->setPermissions(perm);
 			}
 			else
 			{
-				default_perm.initMasks(
-					PERM_NONE, PERM_NONE, PERM_NONE, PERM_NONE, PERM_NONE);
+				LLPermissions default_perm;
+				default_perm.init(LLUUID::null, owner_id, LLUUID::null, LLUUID::null);
+				LLSD llsd_perm_mask = (*it)["perm_mask"];
+				if(llsd_perm_mask.isDefined())
+				{
+					PermissionMask perm_mask = llsd_perm_mask.asInteger();
+					default_perm.initMasks(
+						perm_mask, perm_mask, perm_mask, perm_mask, perm_mask);
+				}
+				else
+				{
+					default_perm.initMasks(
+						PERM_NONE, PERM_NONE, PERM_NONE, PERM_NONE, PERM_NONE);
+				}
+				item->setPermissions(default_perm);
+				item->setAssetUUID(data_id.asUUID());
 			}
-			item->setPermissions(default_perm);
-			item->setAssetUUID(id);
-		}
-		meat = (*it).find("flags");
-		if(meat != no_response)
-		{
-			item->setFlags(strtoul((*meat).second.c_str(), NULL, 0));
+
+			LLSD flags = (*it)["flags"];
+			if(flags.isDefined())
+			{
+				// Not sure how well LLSD.asInteger() maps to 
+				// unsigned long - using strtoul()
+				item->setFlags(strtoul(flags.asString().c_str(), NULL, 0));
+			}
+
+			LLSD time = (*it)["time"];
+			if(time.isDefined())
+			{
+				item->setCreationDate(time.asInteger());
+			}
+			addItem(item);
 		}
-		meat = (*it).find("time");
-		if(meat != no_response)
+		else
 		{
-			item->setCreationDate(atoi((*meat).second.c_str()));
+			llwarns << "Unable to import near " << name.asString() << llendl;
+			rv = false;
 		}
-		addItem(item);
-		continue;
-	clean_item:
-		llwarns << "Unable to import near " << item->getName() << llendl;
-		rv = false;
-		//delete item; // automatic when item is reassigned or destroyed
 	}
 	return rv;
 }
diff --git a/indra/newview/llinventorymodel.h b/indra/newview/llinventorymodel.h
index d73fef72072be177fc1b01325f6a78b5a0536064..fcb3cc737a3e355190a8bb372b65215030dd0251 100644
--- a/indra/newview/llinventorymodel.h
+++ b/indra/newview/llinventorymodel.h
@@ -314,10 +314,8 @@ public:
 
 	// methods to load up inventory skeleton & meat. These are used
 	// during authentication. return true if everything parsed.
-	typedef std::map<std::string, std::string> response_t;
-	typedef std::vector<response_t> options_t;
-	bool loadSkeleton(const options_t& options, const LLUUID& owner_id);
-	bool loadMeat(const options_t& options, const LLUUID& owner_id);
+	bool loadSkeleton(const LLSD& options, const LLUUID& owner_id);
+	bool loadMeat(const LLSD& options, const LLUUID& owner_id);
 
 	// This is a brute force method to rebuild the entire parent-child
 	// relations.
diff --git a/indra/newview/lllogininstance.cpp b/indra/newview/lllogininstance.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..388bf38d61d037be2c121139d42a968b7b517ef3
--- /dev/null
+++ b/indra/newview/lllogininstance.cpp
@@ -0,0 +1,532 @@
+/** 
+ * @file lllogininstance.cpp
+ * @brief Viewer's host for a login connection.
+ *
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * 
+ * Copyright (c) 2009, Linden Research, Inc.
+ * 
+ * Second Life Viewer Source Code
+ * The source code in this file ("Source Code") is provided by Linden Lab
+ * to you under the terms of the GNU General Public License, version 2.0
+ * ("GPL"), unless you have obtained a separate licensing agreement
+ * ("Other License"), formally executed by you and Linden Lab.  Terms of
+ * the GPL can be found in doc/GPL-license.txt in this distribution, or
+ * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
+ * 
+ * There are special exceptions to the terms and conditions of the GPL as
+ * it is applied to this Source Code. View the full text of the exception
+ * in the file doc/FLOSS-exception.txt in this software distribution, or
+ * online at
+ * http://secondlifegrid.net/programs/open_source/licensing/flossexception
+ * 
+ * By copying, modifying or distributing this software, you acknowledge
+ * that you have read and understood your obligations described above,
+ * and agree to abide by those obligations.
+ * 
+ * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
+ * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
+ * COMPLETENESS OR PERFORMANCE.
+ * $/LicenseInfo$
+ */
+
+#include "llviewerprecompiledheaders.h"
+
+#include "lllogininstance.h"
+
+// llcommon
+#include "llevents.h"
+#include "llmd5.h"
+#include "stringize.h"
+
+// llmessage (!)
+#include "llfiltersd2xmlrpc.h" // for xml_escape_string()
+
+// login
+#include "lllogin.h"
+
+// newview
+#include "llviewernetwork.h"
+#include "llappviewer.h" // Wish I didn't have to, but...
+#include "llviewercontrol.h"
+#include "llurlsimstring.h"
+#include "llfloatertos.h"
+#include "llwindow.h"
+
+std::string construct_start_string();
+
+LLLoginInstance::LLLoginInstance() :
+	mLoginModule(new LLLogin()),
+	mLoginState("offline"),
+	mUserInteraction(true),
+	mSkipOptionalUpdate(false),
+	mAttemptComplete(false),
+	mTransferRate(0.0f)
+{
+	mLoginModule->getEventPump().listen("lllogininstance", 
+		boost::bind(&LLLoginInstance::handleLoginEvent, this, _1));
+}
+
+LLLoginInstance::~LLLoginInstance()
+{
+}
+
+
+void LLLoginInstance::connect(const LLSD& credentials)
+{
+	std::vector<std::string> uris;
+	LLViewerLogin::getInstance()->getLoginURIs(uris);
+	connect(uris.front(), credentials);
+}
+
+void LLLoginInstance::connect(const std::string& uri, const LLSD& credentials)
+{
+	constructAuthParams(credentials);
+	mLoginModule->connect(uri, mRequestData);
+}
+
+void LLLoginInstance::reconnect()
+{
+	// Sort of like connect, only using the pre-existing
+	// request params.
+	std::vector<std::string> uris;
+	LLViewerLogin::getInstance()->getLoginURIs(uris);
+	mLoginModule->connect(uris.front(), mRequestData);
+}
+
+void LLLoginInstance::disconnect()
+{
+	mRequestData.clear();
+	mLoginModule->disconnect();
+}
+
+LLSD LLLoginInstance::getResponse() 
+{
+	return mResponseData; 
+}
+
+void LLLoginInstance::constructAuthParams(const LLSD& credentials)
+{
+	// Set up auth request options.
+//#define LL_MINIMIAL_REQUESTED_OPTIONS
+	LLSD requested_options;
+	// *Note: this is where gUserAuth used to be created.
+	requested_options.append("inventory-root");
+	requested_options.append("inventory-skeleton");
+	//requested_options.append("inventory-meat");
+	//requested_options.append("inventory-skel-targets");
+#if (!defined LL_MINIMIAL_REQUESTED_OPTIONS)
+	if(FALSE == gSavedSettings.getBOOL("NoInventoryLibrary"))
+	{
+		requested_options.append("inventory-lib-root");
+		requested_options.append("inventory-lib-owner");
+		requested_options.append("inventory-skel-lib");
+	//	requested_options.append("inventory-meat-lib");
+	}
+
+	requested_options.append("initial-outfit");
+	requested_options.append("gestures");
+	requested_options.append("event_categories");
+	requested_options.append("event_notifications");
+	requested_options.append("classified_categories");
+	//requested_options.append("inventory-targets");
+	requested_options.append("buddy-list");
+	requested_options.append("ui-config");
+#endif
+	requested_options.append("tutorial_setting");
+	requested_options.append("login-flags");
+	requested_options.append("global-textures");
+	if(gSavedSettings.getBOOL("ConnectAsGod"))
+	{
+		gSavedSettings.setBOOL("UseDebugMenus", TRUE);
+		requested_options.append("god-connect");
+	}
+
+	char hashed_mac_string[MD5HEX_STR_SIZE];		/* Flawfinder: ignore */
+	LLMD5 hashed_mac;
+	hashed_mac.update( gMACAddress, MAC_ADDRESS_BYTES );
+	hashed_mac.finalize();
+	hashed_mac.hex_digest(hashed_mac_string);
+
+	// prepend "$1$" to the password to indicate its the md5'd version.
+	std::string dpasswd("$1$");
+	dpasswd.append(credentials["passwd"].asString());
+
+	// (re)initialize the request params with creds.
+	LLSD request_params(credentials);
+	request_params["passwd"] = dpasswd;
+	request_params["start"] = construct_start_string();
+	request_params["skipoptional"] = mSkipOptionalUpdate;
+	request_params["agree_to_tos"] = false; // Always false here. Set true in 
+	request_params["read_critical"] = false; // handleTOSResponse
+	request_params["last_exec_event"] = gLastExecEvent;
+	request_params["mac"] = hashed_mac_string;
+	request_params["version"] = gCurrentVersion; // Includes channel name
+	request_params["channel"] = gSavedSettings.getString("VersionChannelName");
+	request_params["id0"] = LLAppViewer::instance()->getSerialNumber();
+
+	mRequestData["method"] = "login_to_simulator";
+	mRequestData["params"] = request_params;
+	mRequestData["options"] = requested_options;
+}
+
+bool LLLoginInstance::handleLoginEvent(const LLSD& event)
+{
+	std::cout << "LoginListener called!: \n";
+	std::cout << event << "\n";
+
+	if(!(event.has("state") && event.has("progress")))
+	{
+		llerrs << "Unknown message from LLLogin!" << llendl;
+	}
+
+	mLoginState = event["state"].asString();
+	mResponseData = event["data"];
+	
+	if(event.has("transfer_rate"))
+	{
+		mTransferRate = event["transfer_rate"].asReal();
+	}
+
+	if(mLoginState == "offline")
+	{
+		handleLoginFailure(event);
+	}
+	else if(mLoginState == "online")
+	{
+		handleLoginSuccess(event);
+	}
+
+	return false;
+}
+
+bool LLLoginInstance::handleLoginFailure(const LLSD& event)
+{
+	// Login has failed. 
+	// Figure out why and respond...
+	LLSD response = event["data"];
+	std::string reason_response = response["reason"].asString();
+	std::string message_response = response["message"].asString();
+	if(mUserInteraction)
+	{
+		// For the cases of critical message or TOS agreement,
+		// start the TOS dialog. The dialog response will be handled
+		// by the LLLoginInstance::handleTOSResponse() callback.
+		// The callback intiates the login attempt next step, either 
+		// to reconnect or to end the attempt in failure.
+		if(reason_response == "tos")
+		{
+			LLFloaterTOS* tos_dialog = LLFloaterTOS::show(LLFloaterTOS::TOS_TOS,
+											message_response,
+											boost::bind(&LLLoginInstance::handleTOSResponse, 
+														this, _1, "agree_to_tos")
+											);
+			tos_dialog->startModal();
+		}
+		else if(reason_response == "critical")
+		{
+			LLFloaterTOS* tos_dialog = LLFloaterTOS::show(LLFloaterTOS::TOS_CRITICAL_MESSAGE,
+											message_response,
+											boost::bind(&LLLoginInstance::handleTOSResponse, 
+														this, _1, "read_critical")
+											);
+			tos_dialog->startModal();
+		}
+		else if(reason_response == "update" || gSavedSettings.getBOOL("ForceMandatoryUpdate"))
+		{
+			gSavedSettings.setBOOL("ForceMandatoryUpdate", FALSE);
+			updateApp(true, message_response);
+		}
+		else if(reason_response == "optional")
+		{
+			updateApp(false, message_response);
+		}
+		else
+		{	
+			attemptComplete();
+		}	
+	}
+	else // no user interaction
+	{
+		attemptComplete();
+	}
+
+	return false;
+}
+
+bool LLLoginInstance::handleLoginSuccess(const LLSD& event)
+{
+	LLSD response = event["data"];
+	std::string message_response = response["message"].asString();
+	if(gSavedSettings.getBOOL("ForceMandatoryUpdate"))
+	{
+		// Testing update...
+		gSavedSettings.setBOOL("ForceMandatoryUpdate", FALSE);
+		// Don't confuse startup by leaving login "online".
+		mLoginModule->disconnect(); 
+		updateApp(true, message_response);
+	}
+	else
+	{
+		attemptComplete();
+	}
+	return false;
+}
+
+void LLLoginInstance::handleTOSResponse(bool accepted, const std::string& key)
+{
+	if(accepted)
+	{	
+		// Set the request data to true and retry login.
+		mRequestData[key] = true; 
+		reconnect();
+	}
+	else
+	{
+		attemptComplete();
+	}
+}
+
+
+void LLLoginInstance::updateApp(bool mandatory, const std::string& auth_msg)
+{
+	// store off config state, as we might quit soon
+	gSavedSettings.saveToFile(gSavedSettings.getString("ClientSettingsFile"), TRUE);	
+
+	std::ostringstream message;
+
+	//*TODO:translate
+	std::string msg;
+	if (!auth_msg.empty())
+	{
+		msg = "(" + auth_msg + ") \n";
+	}
+
+	LLSD args;
+	args["MESSAGE"] = msg;
+	
+	LLSD payload;
+	payload["mandatory"] = mandatory;
+
+/*
+ We're constructing one of the following 6 strings here:
+	 "DownloadWindowsMandatory"
+	 "DownloadWindowsReleaseForDownload"
+	 "DownloadWindows"
+	 "DownloadMacMandatory"
+	 "DownloadMacReleaseForDownload"
+	 "DownloadMac"
+ 
+ I've called them out explicitly in this comment so that they can be grepped for.
+ 
+ Also, we assume that if we're not Windows we're Mac. If we ever intend to support 
+ Linux with autoupdate, this should be an explicit #elif LL_DARWIN, but 
+ we'd rather deliver the wrong message than no message, so until Linux is supported
+ we'll leave it alone.
+ */
+	std::string notification_name = "Download";
+	
+#if LL_WINDOWS
+	notification_name += "Windows";
+#else
+	notification_name += "Mac";
+#endif
+	
+	if (mandatory)
+	{
+		notification_name += "Mandatory";
+	}
+	else
+	{
+#if LL_RELEASE_FOR_DOWNLOAD
+		notification_name += "ReleaseForDownload";
+#endif
+	}
+	
+	LLNotifications::instance().add(notification_name, args, payload, 
+		boost::bind(&LLLoginInstance::updateDialogCallback, this, _1, _2));
+}
+
+bool LLLoginInstance::updateDialogCallback(const LLSD& notification, const LLSD& response)
+{
+	S32 option = LLNotification::getSelectedOption(notification, response);
+	std::string update_exe_path;
+	bool mandatory = notification["payload"]["mandatory"].asBoolean();
+
+#if !LL_RELEASE_FOR_DOWNLOAD
+	if (option == 2)
+	{
+		// This condition attempts to skip the 
+		// update if using a dev build.
+		// The relog probably won't work if the 
+		// update is mandatory. :)
+
+	    // *REMOVE:Mani - Saving for reference...
+		//LLStartUp::setStartupState( STATE_LOGIN_AUTH_INIT ); 
+		mSkipOptionalUpdate = true;
+		reconnect();
+		return false;
+	}
+#endif
+
+	if (option == 1)
+	{
+		// ...user doesn't want to do it
+		if (mandatory)
+		{
+			// Mandatory update, user chose to not to update...
+			// The login attemp is complete, startup should 
+			// quit when detecting this.
+			attemptComplete();
+
+			// *REMOVE:Mani - Saving for reference...
+			//LLAppViewer::instance()->forceQuit();
+			// // Bump them back to the login screen.
+			// //reset_login();
+		}
+		else
+		{
+			// Optional update, user chose to skip
+			mSkipOptionalUpdate = true;
+			reconnect();
+		}
+		return false;
+	}
+	
+	LLSD query_map = LLSD::emptyMap();
+	// *TODO place os string in a global constant
+#if LL_WINDOWS  
+	query_map["os"] = "win";
+#elif LL_DARWIN
+	query_map["os"] = "mac";
+#elif LL_LINUX
+	query_map["os"] = "lnx";
+#elif LL_SOLARIS
+	query_map["os"] = "sol";
+#endif
+	// *TODO change userserver to be grid on both viewer and sim, since
+	// userserver no longer exists.
+	query_map["userserver"] = LLViewerLogin::getInstance()->getGridLabel();
+	query_map["channel"] = gSavedSettings.getString("VersionChannelName");
+	// *TODO constantize this guy
+	LLURI update_url = LLURI::buildHTTP("secondlife.com", 80, "update.php", query_map);
+	
+	if(LLAppViewer::sUpdaterInfo)
+	{
+		delete LLAppViewer::sUpdaterInfo;
+	}
+	LLAppViewer::sUpdaterInfo = new LLAppViewer::LLUpdaterInfo() ;
+	
+#if LL_WINDOWS
+	LLAppViewer::sUpdaterInfo->mUpdateExePath = gDirUtilp->getTempFilename();
+	if (LLAppViewer::sUpdaterInfo->mUpdateExePath.empty())
+	{
+		delete LLAppViewer::sUpdaterInfo ;
+		LLAppViewer::sUpdaterInfo = NULL ;
+
+		// We're hosed, bail
+		LL_WARNS("AppInit") << "LLDir::getTempFilename() failed" << LL_ENDL;
+
+		attemptComplete();
+		// *REMOVE:Mani - Saving for reference...
+		// LLAppViewer::instance()->forceQuit();
+		return false;
+	}
+
+	LLAppViewer::sUpdaterInfo->mUpdateExePath += ".exe";
+
+	std::string updater_source = gDirUtilp->getAppRODataDir();
+	updater_source += gDirUtilp->getDirDelimiter();
+	updater_source += "updater.exe";
+
+	LL_DEBUGS("AppInit") << "Calling CopyFile source: " << updater_source
+			<< " dest: " << LLAppViewer::sUpdaterInfo->mUpdateExePath
+			<< LL_ENDL;
+
+
+	if (!CopyFileA(updater_source.c_str(), LLAppViewer::sUpdaterInfo->mUpdateExePath.c_str(), FALSE))
+	{
+		delete LLAppViewer::sUpdaterInfo ;
+		LLAppViewer::sUpdaterInfo = NULL ;
+
+		LL_WARNS("AppInit") << "Unable to copy the updater!" << LL_ENDL;
+		attemptComplete();
+		// *REMOVE:Mani - Saving for reference...
+		// LLAppViewer::instance()->forceQuit();
+		return false;
+	}
+
+	// if a sim name was passed in via command line parameter (typically through a SLURL)
+	if ( LLURLSimString::sInstance.mSimString.length() )
+	{
+		// record the location to start at next time
+		gSavedSettings.setString( "NextLoginLocation", LLURLSimString::sInstance.mSimString ); 
+	};
+
+	LLAppViewer::sUpdaterInfo->mParams << "-url \"" << update_url.asString() << "\"";
+
+	LL_DEBUGS("AppInit") << "Calling updater: " << LLAppViewer::sUpdaterInfo->mUpdateExePath << " " << LLAppViewer::sUpdaterInfo->mParams.str() << LL_ENDL;
+
+	//Explicitly remove the marker file, otherwise we pass the lock onto the child process and things get weird.
+	LLAppViewer::instance()->removeMarkerFile(); // In case updater fails
+
+	// *NOTE:Mani The updater is spawned as the last thing before the WinMain exit.
+	// see LLAppViewerWin32.cpp
+	
+#elif LL_DARWIN
+	// if a sim name was passed in via command line parameter (typically through a SLURL)
+	if ( LLURLSimString::sInstance.mSimString.length() )
+	{
+		// record the location to start at next time
+		gSavedSettings.setString( "NextLoginLocation", LLURLSimString::sInstance.mSimString ); 
+	};
+	
+	LLAppViewer::sUpdaterInfo->mUpdateExePath = "'";
+	LLAppViewer::sUpdaterInfo->mUpdateExePath += gDirUtilp->getAppRODataDir();
+	LLAppViewer::sUpdaterInfo->mUpdateExePath += "/mac-updater.app/Contents/MacOS/mac-updater' -url \"";
+	LLAppViewer::sUpdaterInfo->mUpdateExePath += update_url.asString();
+	LLAppViewer::sUpdaterInfo->mUpdateExePath += "\" -name \"";
+	LLAppViewer::sUpdaterInfo->mUpdateExePath += LLAppViewer::instance()->getSecondLifeTitle();
+	LLAppViewer::sUpdaterInfo->mUpdateExePath += "\" &";
+
+	LL_DEBUGS("AppInit") << "Calling updater: " << LLAppViewer::sUpdaterInfo->mUpdateExePath << LL_ENDL;
+
+	// Run the auto-updater.
+	system(LLAppViewer::sUpdaterInfo->mUpdateExePath.c_str()); /* Flawfinder: ignore */
+
+#elif LL_LINUX || LL_SOLARIS
+	OSMessageBox("Automatic updating is not yet implemented for Linux.\n"
+		"Please download the latest version from www.secondlife.com.",
+		LLStringUtil::null, OSMB_OK);
+#endif
+
+	// *REMOVE:Mani - Saving for reference...
+	// LLAppViewer::instance()->forceQuit();
+
+	return false;
+}
+
+std::string construct_start_string()
+{
+	std::string start;
+	if (LLURLSimString::parse())
+	{
+		// a startup URL was specified
+		std::string unescaped_start = 
+			STRINGIZE(  "uri:" 
+						<< LLURLSimString::sInstance.mSimName << "&" 
+						<< LLURLSimString::sInstance.mX << "&" 
+						<< LLURLSimString::sInstance.mY << "&" 
+						<< LLURLSimString::sInstance.mZ);
+		start = xml_escape_string(unescaped_start);
+	}
+	else if (gSavedSettings.getBOOL("LoginLastLocation"))
+	{
+		start = "last";
+	}
+	else
+	{
+		start = "home";
+	}
+	return start;
+}
diff --git a/indra/newview/lllogininstance.h b/indra/newview/lllogininstance.h
new file mode 100644
index 0000000000000000000000000000000000000000..da70fec40e55ae2b0aa4c3da869b1f29c7e66458
--- /dev/null
+++ b/indra/newview/lllogininstance.h
@@ -0,0 +1,95 @@
+/** 
+ * @file lllogininstance.h
+ * @brief A host for the viewer's login connection.
+ *
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * 
+ * Copyright (c) 2009, Linden Research, Inc.
+ * 
+ * Second Life Viewer Source Code
+ * The source code in this file ("Source Code") is provided by Linden Lab
+ * to you under the terms of the GNU General Public License, version 2.0
+ * ("GPL"), unless you have obtained a separate licensing agreement
+ * ("Other License"), formally executed by you and Linden Lab.  Terms of
+ * the GPL can be found in doc/GPL-license.txt in this distribution, or
+ * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
+ * 
+ * There are special exceptions to the terms and conditions of the GPL as
+ * it is applied to this Source Code. View the full text of the exception
+ * in the file doc/FLOSS-exception.txt in this software distribution, or
+ * online at
+ * http://secondlifegrid.net/programs/open_source/licensing/flossexception
+ * 
+ * By copying, modifying or distributing this software, you acknowledge
+ * that you have read and understood your obligations described above,
+ * and agree to abide by those obligations.
+ * 
+ * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
+ * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
+ * COMPLETENESS OR PERFORMANCE.
+ * $/LicenseInfo$
+ */
+
+#ifndef LL_LLLOGININSTANCE_H
+#define LL_LLLOGININSTANCE_H
+
+#include <boost/scoped_ptr.hpp>
+class LLLogin;
+
+// This class hosts the login module and is used to 
+// negotiate user authentication attempts.
+class LLLoginInstance : public LLSingleton<LLLoginInstance>
+{
+public:
+	LLLoginInstance();
+	~LLLoginInstance();
+
+	void connect(const LLSD& credential); // Connect to the current grid choice.
+	void connect(const std::string& uri, const LLSD& credential);	// Connect to the given uri.
+	void reconnect(); // reconnect using the current credentials.
+	void disconnect();
+
+	// Set whether this class will drive user interaction.
+	// If not, login failures like 'need tos agreement' will 
+	// end the login attempt.
+	void setUserInteraction(bool state) { mUserInteraction = state; } 
+	bool getUserInteraction() { return mUserInteraction; }
+
+	// Whether to tell login to skip optional update request.
+	// False by default.
+	void setSkipOptionalUpdate(bool state) { mSkipOptionalUpdate = state; }
+	
+	bool authFailure() { return mAttemptComplete && mLoginState == "offline"; }
+	bool authSuccess() { return mAttemptComplete && mLoginState == "online"; }
+
+	const std::string& getLoginState() { return mLoginState; }
+	LLSD getResponse(const std::string& key) { return getResponse()[key]; }
+	LLSD getResponse();
+
+	// Only valid when authSuccess == true.
+	const F64 getLastTransferRateBPS() { return mTransferRate; }
+
+private:
+	void constructAuthParams(const LLSD& credentials); 
+	void updateApp(bool mandatory, const std::string& message);
+	bool updateDialogCallback(const LLSD& notification, const LLSD& response);
+
+	bool handleLoginEvent(const LLSD& event);
+	bool handleLoginFailure(const LLSD& event);
+	bool handleLoginSuccess(const LLSD& event);
+
+	void handleTOSResponse(bool v, const std::string& key);
+
+	void attemptComplete() { mAttemptComplete = true; } // In the future an event?
+
+	boost::scoped_ptr<LLLogin> mLoginModule;
+	std::string mLoginState;
+	LLSD mRequestData;
+	LLSD mResponseData;
+	bool mUserInteraction; 
+	bool mSkipOptionalUpdate;
+	bool mAttemptComplete;
+	F64 mTransferRate;
+};
+
+#endif
diff --git a/indra/newview/llpanellogin.cpp b/indra/newview/llpanellogin.cpp
index 671d3264bb1eab625615358301a5a0412cb59bc0..06c78a93da20ea80acf0492ef5ed64ceb709e9be 100644
--- a/indra/newview/llpanellogin.cpp
+++ b/indra/newview/llpanellogin.cpp
@@ -434,7 +434,7 @@ BOOL LLPanelLogin::handleKeyHere(KEY key, MASK mask)
 	if ( KEY_F2 == key )
 	{
 		llinfos << "Spawning floater TOS window" << llendl;
-		LLFloaterTOS* tos_dialog = LLFloaterTOS::show(LLFloaterTOS::TOS_TOS,"");
+		LLFloaterTOS* tos_dialog = LLFloaterTOS::show(LLFloaterTOS::TOS_TOS,"", NULL);
 		tos_dialog->startModal();
 		return TRUE;
 	}
diff --git a/indra/newview/llstartup.h b/indra/newview/llstartup.h
index 93701800e9f6f8cb0ef1eabebea9b727ee67d380..5e89030a0114afc91ef2e6fc4abd0db16b6cca52 100644
--- a/indra/newview/llstartup.h
+++ b/indra/newview/llstartup.h
@@ -50,11 +50,7 @@ typedef enum {
 	STATE_LOGIN_SHOW,				// Show login screen
 	STATE_LOGIN_WAIT,				// Wait for user input at login screen
 	STATE_LOGIN_CLEANUP,			// Get rid of login screen and start login
-	STATE_UPDATE_CHECK,				// Wait for user at a dialog box (updates, term-of-service, etc)
 	STATE_LOGIN_AUTH_INIT,			// Start login to SL servers
-	STATE_LOGIN_AUTHENTICATE,		// Do authentication voodoo
-	STATE_LOGIN_NO_DATA_YET,		// Waiting for authentication replies to start
-	STATE_LOGIN_DOWNLOADING,		// Waiting for authentication replies to download
 	STATE_LOGIN_PROCESS_RESPONSE,	// Check authentication reply
 	STATE_WORLD_INIT,				// Start building the world
 	STATE_MULTIMEDIA_INIT,			// Init the rest of multimedia library
@@ -75,8 +71,6 @@ typedef enum {
 // exported symbols
 extern bool gAgentMovementCompleted;
 extern LLPointer<LLImageGL> gStartImageGL;
-extern std::string gInitialOutfit;
-extern std::string gInitialOutfitGender;	// "male" or "female"
 
 class LLStartUp
 {
diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp
index f70e5ad2427a8162c89c26a1e6a10b9bb3942941..5647b6889b0d31e006ae0dbd83e33d9714109a2d 100644
--- a/indra/newview/llviewermenu.cpp
+++ b/indra/newview/llviewermenu.cpp
@@ -177,7 +177,6 @@
 #include "lltrans.h"
 #include "lluictrlfactory.h"
 #include "lluploaddialog.h"
-#include "lluserauth.h"
 #include "lluuid.h"
 #include "llviewercamera.h"
 #include "llviewergenericmessage.h"
diff --git a/indra/newview/llviewernetwork.cpp b/indra/newview/llviewernetwork.cpp
index 918b15ef09dc5a211b4ecb64c42bc16ebe740e0e..801c46035a20e2c438d23c9b8c70ba2edb67cd54 100644
--- a/indra/newview/llviewernetwork.cpp
+++ b/indra/newview/llviewernetwork.cpp
@@ -35,6 +35,8 @@
 
 #include "llviewernetwork.h"
 #include "llviewercontrol.h"
+#include "llevents.h"
+#include "lllogin.h"
 
 struct LLGridData
 {
@@ -155,6 +157,10 @@ LLViewerLogin::LLViewerLogin() :
 {
 }
 
+ LLViewerLogin::~LLViewerLogin() 
+ {
+ }
+
 void LLViewerLogin::setGridChoice(EGridInfo grid)
 {	
 	if(grid < 0 || grid >= GRID_INFO_COUNT)
diff --git a/indra/newview/llviewernetwork.h b/indra/newview/llviewernetwork.h
index 4001ed05c1e5f25eb09cf7f2edbc5db20992e598..edae6dc47b56b87f1770269bf2fde8b61261de56 100644
--- a/indra/newview/llviewernetwork.h
+++ b/indra/newview/llviewernetwork.h
@@ -34,7 +34,10 @@
 #ifndef LL_LLVIEWERNETWORK_H
 #define LL_LLVIEWERNETWORK_H
 
+#include <boost/scoped_ptr.hpp>
+
 class LLHost;
+class LLLogin;
 
 enum EGridInfo
 {
@@ -74,6 +77,7 @@ class LLViewerLogin : public LLSingleton<LLViewerLogin>
 {
 public:
 	LLViewerLogin();
+	~LLViewerLogin();
 
 	void setGridChoice(EGridInfo grid);
 	void setGridChoice(const std::string& grid_name);
diff --git a/indra/newview/llxmlrpclistener.cpp b/indra/newview/llxmlrpclistener.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..2821e6c59f99fa3781f6db41ca704eeefc00d60a
--- /dev/null
+++ b/indra/newview/llxmlrpclistener.cpp
@@ -0,0 +1,494 @@
+/**
+ * @file   llxmlrpclistener.cpp
+ * @author Nat Goodspeed
+ * @date   2009-03-18
+ * @brief  Implementation for llxmlrpclistener.
+ * 
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * Copyright (c) 2009, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+
+// Precompiled header
+#include "llviewerprecompiledheaders.h"
+// associated header
+#include "llxmlrpclistener.h"
+// STL headers
+#include <map>
+#include <set>
+// std headers
+// external library headers
+#include <boost/scoped_ptr.hpp>
+#include <boost/range.hpp>          // boost::begin(), boost::end()
+// other Linden headers
+#include "llerror.h"
+#include "stringize.h"
+#include "llxmlrpctransaction.h"
+
+#include <xmlrpc-epi/xmlrpc.h>
+
+#if LL_WINDOWS
+#pragma warning (disable : 4355) // 'this' used in initializer list: yes, intentionally
+#endif
+
+template <typename STATUS>
+class StatusMapperBase
+{
+    typedef std::map<STATUS, std::string> MapType;
+
+public:
+    StatusMapperBase(const std::string& desc):
+        mDesc(desc)
+    {}
+
+    std::string lookup(STATUS status) const
+    {
+        typename MapType::const_iterator found = mMap.find(status);
+        if (found != mMap.end())
+        {
+            return found->second;
+        }
+        return STRINGIZE("<unknown " << mDesc << " " << status << ">");
+    }
+
+protected:
+    std::string mDesc;
+    MapType mMap;
+};
+
+class StatusMapper: public StatusMapperBase<LLXMLRPCTransaction::Status>
+{
+public:
+    StatusMapper(): StatusMapperBase<LLXMLRPCTransaction::Status>("Status")
+    {
+		mMap[LLXMLRPCTransaction::StatusNotStarted]  = "NotStarted";
+		mMap[LLXMLRPCTransaction::StatusStarted]     = "Started";
+		mMap[LLXMLRPCTransaction::StatusDownloading] = "Downloading";
+		mMap[LLXMLRPCTransaction::StatusComplete]    = "Complete";
+		mMap[LLXMLRPCTransaction::StatusCURLError]   = "CURLError";
+		mMap[LLXMLRPCTransaction::StatusXMLRPCError] = "XMLRPCError";
+		mMap[LLXMLRPCTransaction::StatusOtherError]  = "OtherError";
+    }
+};
+
+static const StatusMapper sStatusMapper;
+
+class CURLcodeMapper: public StatusMapperBase<CURLcode>
+{
+public:
+    CURLcodeMapper(): StatusMapperBase<CURLcode>("CURLcode")
+    {
+        // from curl.h
+// skip the "CURLE_" prefix for each of these strings
+#define def(sym) (mMap[sym] = #sym + 6)
+        def(CURLE_OK);
+        def(CURLE_UNSUPPORTED_PROTOCOL);    /* 1 */
+        def(CURLE_FAILED_INIT);             /* 2 */
+        def(CURLE_URL_MALFORMAT);           /* 3 */
+        def(CURLE_URL_MALFORMAT_USER);      /* 4 - NOT USED */
+        def(CURLE_COULDNT_RESOLVE_PROXY);   /* 5 */
+        def(CURLE_COULDNT_RESOLVE_HOST);    /* 6 */
+        def(CURLE_COULDNT_CONNECT);         /* 7 */
+        def(CURLE_FTP_WEIRD_SERVER_REPLY);  /* 8 */
+        def(CURLE_FTP_ACCESS_DENIED);       /* 9 a service was denied by the FTP server
+                                          due to lack of access - when login fails
+                                          this is not returned. */
+        def(CURLE_FTP_USER_PASSWORD_INCORRECT); /* 10 - NOT USED */
+        def(CURLE_FTP_WEIRD_PASS_REPLY);    /* 11 */
+        def(CURLE_FTP_WEIRD_USER_REPLY);    /* 12 */
+        def(CURLE_FTP_WEIRD_PASV_REPLY);    /* 13 */
+        def(CURLE_FTP_WEIRD_227_FORMAT);    /* 14 */
+        def(CURLE_FTP_CANT_GET_HOST);       /* 15 */
+        def(CURLE_FTP_CANT_RECONNECT);      /* 16 */
+        def(CURLE_FTP_COULDNT_SET_BINARY);  /* 17 */
+        def(CURLE_PARTIAL_FILE);            /* 18 */
+        def(CURLE_FTP_COULDNT_RETR_FILE);   /* 19 */
+        def(CURLE_FTP_WRITE_ERROR);         /* 20 */
+        def(CURLE_FTP_QUOTE_ERROR);         /* 21 */
+        def(CURLE_HTTP_RETURNED_ERROR);     /* 22 */
+        def(CURLE_WRITE_ERROR);             /* 23 */
+        def(CURLE_MALFORMAT_USER);          /* 24 - NOT USED */
+        def(CURLE_UPLOAD_FAILED);           /* 25 - failed upload "command" */
+        def(CURLE_READ_ERROR);              /* 26 - could open/read from file */
+        def(CURLE_OUT_OF_MEMORY);           /* 27 */
+        /* Note: CURLE_OUT_OF_MEMORY may sometimes indicate a conversion error
+                 instead of a memory allocation error if CURL_DOES_CONVERSIONS
+                 is defined
+        */
+        def(CURLE_OPERATION_TIMEOUTED);     /* 28 - the timeout time was reached */
+        def(CURLE_FTP_COULDNT_SET_ASCII);   /* 29 - TYPE A failed */
+        def(CURLE_FTP_PORT_FAILED);         /* 30 - FTP PORT operation failed */
+        def(CURLE_FTP_COULDNT_USE_REST);    /* 31 - the REST command failed */
+        def(CURLE_FTP_COULDNT_GET_SIZE);    /* 32 - the SIZE command failed */
+        def(CURLE_HTTP_RANGE_ERROR);        /* 33 - RANGE "command" didn't work */
+        def(CURLE_HTTP_POST_ERROR);         /* 34 */
+        def(CURLE_SSL_CONNECT_ERROR);       /* 35 - wrong when connecting with SSL */
+        def(CURLE_BAD_DOWNLOAD_RESUME);     /* 36 - couldn't resume download */
+        def(CURLE_FILE_COULDNT_READ_FILE);  /* 37 */
+        def(CURLE_LDAP_CANNOT_BIND);        /* 38 */
+        def(CURLE_LDAP_SEARCH_FAILED);      /* 39 */
+        def(CURLE_LIBRARY_NOT_FOUND);       /* 40 */
+        def(CURLE_FUNCTION_NOT_FOUND);      /* 41 */
+        def(CURLE_ABORTED_BY_CALLBACK);     /* 42 */
+        def(CURLE_BAD_FUNCTION_ARGUMENT);   /* 43 */
+        def(CURLE_BAD_CALLING_ORDER);       /* 44 - NOT USED */
+        def(CURLE_INTERFACE_FAILED);        /* 45 - CURLOPT_INTERFACE failed */
+        def(CURLE_BAD_PASSWORD_ENTERED);    /* 46 - NOT USED */
+        def(CURLE_TOO_MANY_REDIRECTS );     /* 47 - catch endless re-direct loops */
+        def(CURLE_UNKNOWN_TELNET_OPTION);   /* 48 - User specified an unknown option */
+        def(CURLE_TELNET_OPTION_SYNTAX );   /* 49 - Malformed telnet option */
+        def(CURLE_OBSOLETE);                /* 50 - NOT USED */
+        def(CURLE_SSL_PEER_CERTIFICATE);    /* 51 - peer's certificate wasn't ok */
+        def(CURLE_GOT_NOTHING);             /* 52 - when this is a specific error */
+        def(CURLE_SSL_ENGINE_NOTFOUND);     /* 53 - SSL crypto engine not found */
+        def(CURLE_SSL_ENGINE_SETFAILED);    /* 54 - can not set SSL crypto engine as
+                                          default */
+        def(CURLE_SEND_ERROR);              /* 55 - failed sending network data */
+        def(CURLE_RECV_ERROR);              /* 56 - failure in receiving network data */
+        def(CURLE_SHARE_IN_USE);            /* 57 - share is in use */
+        def(CURLE_SSL_CERTPROBLEM);         /* 58 - problem with the local certificate */
+        def(CURLE_SSL_CIPHER);              /* 59 - couldn't use specified cipher */
+        def(CURLE_SSL_CACERT);              /* 60 - problem with the CA cert (path?) */
+        def(CURLE_BAD_CONTENT_ENCODING);    /* 61 - Unrecognized transfer encoding */
+        def(CURLE_LDAP_INVALID_URL);        /* 62 - Invalid LDAP URL */
+        def(CURLE_FILESIZE_EXCEEDED);       /* 63 - Maximum file size exceeded */
+        def(CURLE_FTP_SSL_FAILED);          /* 64 - Requested FTP SSL level failed */
+        def(CURLE_SEND_FAIL_REWIND);        /* 65 - Sending the data requires a rewind
+                                          that failed */
+        def(CURLE_SSL_ENGINE_INITFAILED);   /* 66 - failed to initialise ENGINE */
+        def(CURLE_LOGIN_DENIED);            /* 67 - user); password or similar was not
+                                          accepted and we failed to login */
+        def(CURLE_TFTP_NOTFOUND);           /* 68 - file not found on server */
+        def(CURLE_TFTP_PERM);               /* 69 - permission problem on server */
+        def(CURLE_TFTP_DISKFULL);           /* 70 - out of disk space on server */
+        def(CURLE_TFTP_ILLEGAL);            /* 71 - Illegal TFTP operation */
+        def(CURLE_TFTP_UNKNOWNID);          /* 72 - Unknown transfer ID */
+        def(CURLE_TFTP_EXISTS);             /* 73 - File already exists */
+        def(CURLE_TFTP_NOSUCHUSER);         /* 74 - No such user */
+        def(CURLE_CONV_FAILED);             /* 75 - conversion failed */
+        def(CURLE_CONV_REQD);               /* 76 - caller must register conversion
+                                          callbacks using curl_easy_setopt options
+                                          CURLOPT_CONV_FROM_NETWORK_FUNCTION);
+                                          CURLOPT_CONV_TO_NETWORK_FUNCTION); and
+                                          CURLOPT_CONV_FROM_UTF8_FUNCTION */
+        def(CURLE_SSL_CACERT_BADFILE);      /* 77 - could not load CACERT file); missing
+                                          or wrong format */
+        def(CURLE_REMOTE_FILE_NOT_FOUND);   /* 78 - remote file not found */
+        def(CURLE_SSH);                     /* 79 - error from the SSH layer); somewhat
+                                          generic so the error message will be of
+                                          interest when this has happened */
+
+        def(CURLE_SSL_SHUTDOWN_FAILED);     /* 80 - Failed to shut down the SSL
+                                          connection */
+#undef  def
+    }
+};
+
+static const CURLcodeMapper sCURLcodeMapper;
+
+LLXMLRPCListener::LLXMLRPCListener(const std::string& pumpname):
+    mBoundListener(LLEventPumps::instance().
+                   obtain(pumpname).
+                   listen("LLXMLRPCListener", boost::bind(&LLXMLRPCListener::process, this, _1)))
+{
+}
+
+/**
+ * Capture an outstanding LLXMLRPCTransaction and poll it periodically until
+ * done.
+ *
+ * The sequence is:
+ * # Instantiate Poller, which instantiates, populates and initiates an
+ *   LLXMLRPCTransaction. Poller self-registers on the LLEventPump named
+ *   "mainloop".
+ * # "mainloop" is conventionally pumped once per frame. On each such call,
+ *   Poller checks its LLXMLRPCTransaction for completion.
+ * # When the LLXMLRPCTransaction completes, Poller collects results (if any)
+ *   and sends notification.
+ * # The tricky part: Poller frees itself (and thus its LLXMLRPCTransaction)
+ *   when done. The only external reference to it is the connection to the
+ *   "mainloop" LLEventPump.
+ */
+class Poller
+{
+public:
+    /// Validate the passed request for required fields, then use it to
+    /// populate an XMLRPC_REQUEST and an associated LLXMLRPCTransaction. Send
+    /// the request.
+    Poller(const LLSD& command):
+        mUri(command["uri"]),
+        mMethod(command["method"]),
+        mReplyPump(command["reply"])
+    {
+        // LL_ERRS if any of these are missing
+        const char* required[] = { "uri", "method", "reply" };
+        // optional: "options" (array of string)
+        // Validate the request
+        std::set<std::string> missing;
+        for (const char** ri = boost::begin(required); ri != boost::end(required); ++ri)
+        {
+            // If the command does not contain this required entry, add it to 'missing'.
+            if (! command.has(*ri))
+            {
+                missing.insert(*ri);
+            }
+        }
+        if (! missing.empty())
+        {
+            LL_ERRS("LLXMLRPCListener") << mMethod << " request missing params: ";
+            const char* separator = "";
+            for (std::set<std::string>::const_iterator mi(missing.begin()), mend(missing.end());
+                 mi != mend; ++mi)
+            {
+                LL_CONT << separator << *mi;
+                separator = ", ";
+            }
+            LL_CONT << LL_ENDL;
+        }
+
+        // Build the XMLRPC request.
+        XMLRPC_REQUEST request = XMLRPC_RequestNew();
+        XMLRPC_RequestSetMethodName(request, mMethod.c_str());
+        XMLRPC_RequestSetRequestType(request, xmlrpc_request_call);
+        XMLRPC_VALUE xparams = XMLRPC_CreateVector(NULL, xmlrpc_vector_struct);
+        LLSD params(command["params"]);
+        if (params.isMap())
+        {
+            for (LLSD::map_const_iterator pi(params.beginMap()), pend(params.endMap());
+                 pi != pend; ++pi)
+            {
+                std::string name(pi->first);
+                LLSD param(pi->second);
+                if (param.isString())
+                {
+                    XMLRPC_VectorAppendString(xparams, name.c_str(), param.asString().c_str(), 0);
+                }
+                else if (param.isInteger() || param.isBoolean())
+                {
+                    XMLRPC_VectorAppendInt(xparams, name.c_str(), param.asInteger());
+                }
+                else if (param.isReal())
+                {
+                    XMLRPC_VectorAppendDouble(xparams, name.c_str(), param.asReal());
+                }
+                else
+                {
+                    LL_ERRS("LLXMLRPCListener") << mMethod << " request param "
+                                                << name << " has unknown type: " << param << LL_ENDL;
+                }
+            }
+        }
+        LLSD options(command["options"]);
+        if (options.isArray())
+        {
+            XMLRPC_VALUE xoptions = XMLRPC_CreateVector("options", xmlrpc_vector_array);
+            for (LLSD::array_const_iterator oi(options.beginArray()), oend(options.endArray());
+                 oi != oend; ++oi)
+            {
+                XMLRPC_VectorAppendString(xoptions, NULL, oi->asString().c_str(), 0);
+            }
+            XMLRPC_AddValueToVector(xparams, xoptions);
+        }
+        XMLRPC_RequestSetData(request, xparams);
+
+        mTransaction.reset(new LLXMLRPCTransaction(mUri, request));
+		mPreviousStatus = mTransaction->status(NULL);
+
+        // Free the XMLRPC_REQUEST object and the attached data values.
+        XMLRPC_RequestFree(request, 1);
+
+        // Now ensure that we get regular callbacks to poll for completion.
+        mBoundListener =
+            LLEventPumps::instance().
+            obtain("mainloop").
+            listen(LLEventPump::inventName(), boost::bind(&Poller::poll, this, _1));
+
+        LL_INFOS("LLXMLRPCListener") << mMethod << " request sent to " << mUri << LL_ENDL;
+    }
+
+    /// called by "mainloop" LLEventPump
+    bool poll(const LLSD&)
+    {
+        bool done = mTransaction->process();
+
+        CURLcode curlcode;
+        LLXMLRPCTransaction::Status status;
+        {
+            // LLXMLRPCTransaction::status() is defined to accept int* rather
+            // than CURLcode*. I don't feel the urge to fix the signature, but
+            // we want a CURLcode rather than an int. So fetch it as a local
+            // int, but then assign to a CURLcode for the remainder of this
+            // method.
+            int curlint;
+            status = mTransaction->status(&curlint);
+            curlcode = CURLcode(curlint);
+        }
+
+        LLSD data;
+        data["status"] = sStatusMapper.lookup(status);
+        data["errorcode"] = sCURLcodeMapper.lookup(curlcode);
+        data["error"] = "";
+        data["transfer_rate"] = 0.0;
+        LLEventPump& replyPump(LLEventPumps::instance().obtain(mReplyPump));
+		if (! done)
+        {
+            // Not done yet, carry on.
+			if (status == LLXMLRPCTransaction::StatusDownloading
+				&& status != mPreviousStatus)
+			{
+				// If a response has been received, send the 
+				// 'downloading' status if it hasn't been sent.
+				replyPump.post(data);
+			}
+
+			mPreviousStatus = status;
+            return false;
+        }
+
+        // Here the transaction is complete. Check status.
+        data["error"] = mTransaction->statusMessage();
+		data["transfer_rate"] = mTransaction->transferRate();
+        LL_INFOS("LLXMLRPCListener") << mMethod << " result from " << mUri << ": status "
+                                     << data["status"].asString() << ", errorcode "
+                                     << data["errorcode"].asString()
+                                     << " (" << data["error"].asString() << ")"
+                                     << LL_ENDL;
+        // In addition to CURLE_OK, LLUserAuth distinguishes different error
+        // values of 'curlcode':
+        // CURLE_COULDNT_RESOLVE_HOST,
+        // CURLE_SSL_PEER_CERTIFICATE,
+        // CURLE_SSL_CACERT,
+        // CURLE_SSL_CONNECT_ERROR.
+        // Given 'message', need we care?
+        if (status == LLXMLRPCTransaction::StatusComplete)
+        {
+            // Success! Parse data.
+            std::string status_string(data["status"]);
+            data["responses"] = parseResponse(status_string);
+            data["status"] = status_string;
+        }
+
+        // whether successful or not, send reply on requested LLEventPump
+        replyPump.post(data);
+
+        // Because mTransaction is a boost::scoped_ptr, deleting this object
+        // frees our LLXMLRPCTransaction object.
+        // Because mBoundListener is an LLTempBoundListener, deleting this
+        // object disconnects it from "mainloop".
+        // *** MUST BE LAST ***
+        delete this;
+        return false;
+    }
+
+private:
+    /// Derived from LLUserAuth::parseResponse() and parseOptionInto()
+    LLSD parseResponse(std::string& status_string)
+    {
+        // Extract every member into data["responses"] (a map of string
+        // values).
+        XMLRPC_REQUEST response = mTransaction->response();
+        if (! response)
+        {
+            LL_DEBUGS("LLXMLRPCListener") << "No response" << LL_ENDL;
+            return LLSD();
+        }
+
+        XMLRPC_VALUE param = XMLRPC_RequestGetData(response);
+        if (! param)
+        {
+            LL_DEBUGS("LLXMLRPCListener") << "Response contains no data" << LL_ENDL;
+            return LLSD();
+        }
+
+        // Now, parse everything
+        return parseValues(status_string, "", param);
+    }
+
+    /**
+     * Parse key/value pairs from a given XMLRPC_VALUE into an LLSD map.
+     * @param key_pfx Used to describe a given key in log messages. At top
+     * level, pass "". When parsing an options array, pass the top-level key
+     * name of the array plus the index of the array entry; to this we'll
+     * append the subkey of interest.
+     * @param param XMLRPC_VALUE iterator. At top level, pass
+     * XMLRPC_RequestGetData(XMLRPC_REQUEST).
+     */
+    LLSD parseValues(std::string& status_string, const std::string& key_pfx, XMLRPC_VALUE param)
+    {
+        LLSD responses;
+        for (XMLRPC_VALUE current = XMLRPC_VectorRewind(param); current;
+             current = XMLRPC_VectorNext(param))
+        {
+            std::string key(XMLRPC_GetValueID(current));
+            LL_DEBUGS("LLXMLRPCListener") << "key: " << key_pfx << key << LL_ENDL;
+            XMLRPC_VALUE_TYPE_EASY type = XMLRPC_GetValueTypeEasy(current);
+            if (xmlrpc_type_string == type)
+            {
+                LLSD::String val(XMLRPC_GetValueString(current));
+                LL_DEBUGS("LLXMLRPCListener") << "val: " << val << LL_ENDL;
+                responses.insert(key, val);
+            }
+            else if (xmlrpc_type_int == type)
+            {
+                LLSD::Integer val(XMLRPC_GetValueInt(current));
+                LL_DEBUGS("LLXMLRPCListener") << "val: " << val << LL_ENDL;
+                responses.insert(key, val);
+            }
+            else if (xmlrpc_type_double == type)
+            {
+                LLSD::Real val(XMLRPC_GetValueDouble(current));
+                LL_DEBUGS("LLXMLRPCListener") << "val: " << val << LL_ENDL;
+                responses.insert(key, val);
+            }
+            else if (xmlrpc_type_array == type)
+            {
+                // We expect this to be an array of submaps. Walk the array,
+                // recursively parsing each submap and collecting them.
+                LLSD array;
+                int i = 0;          // for descriptive purposes
+                for (XMLRPC_VALUE row = XMLRPC_VectorRewind(current); row;
+                     row = XMLRPC_VectorNext(current), ++i)
+                {
+                    // Recursive call. For the lower-level key_pfx, if 'key'
+                    // is "foo", pass "foo[0]:", then "foo[1]:", etc. In the
+                    // nested call, a subkey "bar" will then be logged as
+                    // "foo[0]:bar", and so forth.
+                    // Parse the scalar subkey/value pairs from this array
+                    // entry into a temp submap. Collect such submaps in 'array'.
+                    array.append(parseValues(status_string,
+                                             STRINGIZE(key_pfx << key << '[' << i << "]:"),
+                                             row));
+                }
+                // Having collected an 'array' of 'submap's, insert that whole
+                // 'array' as the value of this 'key'.
+                responses.insert(key, array);
+            }
+            else
+            {
+                // whoops - unrecognized type
+                LL_WARNS("LLXMLRPCListener") << "Unhandled xmlrpc type " << type << " for key "
+                                             << key_pfx << key << LL_ENDL;
+                responses.insert(key, STRINGIZE("<bad XMLRPC type " << type << '>'));
+                status_string = "BadType";
+            }
+        }
+        return responses;
+    }
+
+    const std::string mUri;
+    const std::string mMethod;
+    const std::string mReplyPump;
+    LLTempBoundListener mBoundListener;
+    boost::scoped_ptr<LLXMLRPCTransaction> mTransaction;
+	LLXMLRPCTransaction::Status mPreviousStatus; // To detect state changes.
+};
+
+bool LLXMLRPCListener::process(const LLSD& command)
+{
+    // Allocate a new heap Poller, but do not save a pointer to it. Poller
+    // will check its own status and free itself on completion of the request.
+    (new Poller(command));
+    // conventional event listener return
+    return false;
+}
diff --git a/indra/newview/llxmlrpclistener.h b/indra/newview/llxmlrpclistener.h
new file mode 100644
index 0000000000000000000000000000000000000000..120c2b329be5413c6279fb546e3b16ef819c6844
--- /dev/null
+++ b/indra/newview/llxmlrpclistener.h
@@ -0,0 +1,35 @@
+/**
+ * @file   llxmlrpclistener.h
+ * @author Nat Goodspeed
+ * @date   2009-03-18
+ * @brief  LLEventPump API for LLXMLRPCTransaction. This header doesn't
+ *         actually define the API; the API is defined by the pump name on
+ *         which this class listens, and by the expected content of LLSD it
+ *         receives.
+ * 
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * Copyright (c) 2009, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_LLXMLRPCLISTENER_H)
+#define LL_LLXMLRPCLISTENER_H
+
+#include "llevents.h"
+
+/// Listen on an LLEventPump with specified name for LLXMLRPCTransaction
+/// request events.
+class LLXMLRPCListener
+{
+public:
+    /// Specify the pump name on which to listen
+    LLXMLRPCListener(const std::string& pumpname);
+
+    /// Handle request events on the event pump specified at construction time
+    bool process(const LLSD& command);
+
+private:
+    LLTempBoundListener mBoundListener;
+};
+
+#endif /* ! defined(LL_LLXMLRPCLISTENER_H) */
diff --git a/indra/newview/llxmlrpctransaction.cpp b/indra/newview/llxmlrpctransaction.cpp
index a2fd0f0d9c25f85b0a5230142c65eda743b703c8..0e1beb377f674ef97096af5c8afd604dc8c63e11 100644
--- a/indra/newview/llxmlrpctransaction.cpp
+++ b/indra/newview/llxmlrpctransaction.cpp
@@ -33,6 +33,7 @@
 #include "llviewerprecompiledheaders.h"
 
 #include "llxmlrpctransaction.h"
+#include "llxmlrpclistener.h"
 
 #include "llcurl.h"
 #include "llviewercontrol.h"
@@ -42,6 +43,13 @@
 
 #include "llappviewer.h"
 
+// Static instance of LLXMLRPCListener declared here so that every time we
+// bring in this code, we instantiate a listener. If we put the static
+// instance of LLXMLRPCListener into llxmlrpclistener.cpp, the linker would
+// simply omit llxmlrpclistener.o, and shouting on the LLEventPump would do
+// nothing.
+static LLXMLRPCListener listener("LLXMLRPCTransaction");
+
 LLXMLRPCValue LLXMLRPCValue::operator[](const char* id) const
 {
 	return LLXMLRPCValue(XMLRPC_VectorGetValueWithID(mV, id));
@@ -213,6 +221,11 @@ LLXMLRPCTransaction::Impl::Impl(const std::string& uri,
 	XMLRPC_RequestSetData(request, params.getValue());
 	
 	init(request, useGzip);
+    // DEV-28398: without this XMLRPC_RequestFree() call, it looks as though
+    // the 'request' object is simply leaked. It's less clear to me whether we
+    // should also ask to free request value data (second param 1), since the
+    // data come from 'params'.
+    XMLRPC_RequestFree(request, 1);
 }
 
 
diff --git a/indra/newview/tests/llcapabilitylistener_test.cpp b/indra/newview/tests/llcapabilitylistener_test.cpp
index 3c5f6fad2d63260a445398c850793e941c23c9f2..90cc8678524e365b44efb1b7086a8dcdd0f9eeb6 100644
--- a/indra/newview/tests/llcapabilitylistener_test.cpp
+++ b/indra/newview/tests/llcapabilitylistener_test.cpp
@@ -24,9 +24,9 @@
 #include "../test/lltut.h"
 #include "../llcapabilityprovider.h"
 #include "lluuid.h"
-#include "llerrorcontrol.h"
 #include "tests/networkio.h"
 #include "tests/commtest.h"
+#include "tests/wrapllerrs.h"
 #include "stringize.h"
 
 #if defined(LL_WINDOWS)
@@ -104,28 +104,6 @@ namespace tut
     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>()
     {
@@ -137,10 +115,10 @@ namespace tut
         std::string threw;
         try
         {
-            CaptureError capture;
+            WrapLL_ERRS capture;
             regionPump.post(request);
         }
-        catch (const CaptureError::FatalException& e)
+        catch (const WrapLL_ERRS::FatalException& e)
         {
             threw = e.what();
         }
@@ -184,10 +162,10 @@ namespace tut
         std::string threw;
         try
         {
-            CaptureError capture;
+            WrapLL_ERRS capture;
             regionPump.post(request);
         }
-        catch (const CaptureError::FatalException& e)
+        catch (const WrapLL_ERRS::FatalException& e)
         {
             threw = e.what();
         }
@@ -246,10 +224,10 @@ namespace tut
         std::string threw;
         try
         {
-            CaptureError capture;
+            WrapLL_ERRS capture;
             regionPump.post(request);
         }
-        catch (const CaptureError::FatalException& e)
+        catch (const WrapLL_ERRS::FatalException& e)
         {
             threw = e.what();
         }
diff --git a/indra/newview/tests/llxmlrpclistener_test.cpp b/indra/newview/tests/llxmlrpclistener_test.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0c1ee42ffcd16d1322a315940f1283e338682c83
--- /dev/null
+++ b/indra/newview/tests/llxmlrpclistener_test.cpp
@@ -0,0 +1,230 @@
+/*
+ * @file   llxmlrpclistener_test.cpp
+ * @author Nat Goodspeed
+ * @date   2009-03-20
+ * @brief  Test for llxmlrpclistener.
+ * 
+ * $LicenseInfo:firstyear=2009&license=internal$
+ * Copyright (c) 2009, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "../llviewerprecompiledheaders.h"
+// associated header
+#include "../llxmlrpclistener.h"
+// STL headers
+#include <iomanip>
+// std headers
+// external library headers
+// other Linden headers
+#include "../test/lltut.h"
+#include "../llxmlrpctransaction.h"
+#include "llevents.h"
+#include "lleventfilter.h"
+#include "llsd.h"
+#include "llcontrol.h"
+#include "tests/wrapllerrs.h"
+
+LLControlGroup gSavedSettings;
+
+/*****************************************************************************
+*   TUT
+*****************************************************************************/
+namespace tut
+{
+    struct data
+    {
+        data():
+            pumps(LLEventPumps::instance()),
+            uri("http://127.0.0.1:8000")
+        {
+            // These variables are required by machinery used by
+            // LLXMLRPCTransaction. The values reflect reality for this test
+            // executable; hopefully these values are correct.
+            gSavedSettings.declareBOOL("BrowserProxyEnabled", FALSE, "", FALSE); // don't persist
+            gSavedSettings.declareBOOL("NoVerifySSLCert", TRUE, "", FALSE); // don't persist
+        }
+
+        // LLEventPump listener signature
+        bool captureReply(const LLSD& r)
+        {
+            reply = r;
+            return false;
+        }
+
+        LLSD reply;
+        LLEventPumps& pumps;
+        std::string uri;
+    };
+    typedef test_group<data> llxmlrpclistener_group;
+    typedef llxmlrpclistener_group::object object;
+    llxmlrpclistener_group llxmlrpclistenergrp("llxmlrpclistener");
+
+    template<> template<>
+    void object::test<1>()
+    {
+        set_test_name("request validation");
+        WrapLL_ERRS capture;
+        LLSD request;
+        request["uri"] = uri;
+        std::string threw;
+        try
+        {
+            pumps.obtain("LLXMLRPCTransaction").post(request);
+        }
+        catch (const WrapLL_ERRS::FatalException& e)
+        {
+            threw = e.what();
+        }
+        ensure_contains("threw exception", threw, "missing params");
+        ensure_contains("identified missing", threw, "method");
+        ensure_contains("identified missing", threw, "reply");
+    }
+
+    template<> template<>
+    void object::test<2>()
+    {
+        set_test_name("param types validation");
+        WrapLL_ERRS capture;
+        LLSD request;
+        request["uri"] = uri;
+        request["method"] = "hello";
+        request["reply"] = "reply";
+        LLSD& params(request["params"]);
+        params["who"]["specifically"] = "world"; // LLXMLRPCListener only handles scalar params
+        std::string threw;
+        try
+        {
+            pumps.obtain("LLXMLRPCTransaction").post(request);
+        }
+        catch (const WrapLL_ERRS::FatalException& e)
+        {
+            threw = e.what();
+        }
+        ensure_contains("threw exception", threw, "unknown type");
+    }
+
+    template<> template<>
+    void object::test<3>()
+    {
+        set_test_name("success case");
+        LLSD request;
+        request["uri"] = uri;
+        request["method"] = "hello";
+        request["reply"] = "reply";
+        LLSD& params(request["params"]);
+        params["who"] = "world";
+        // Set up a timeout filter so we don't spin forever waiting.
+        LLEventTimeout watchdog;
+        // Connect the timeout filter to the reply pump.
+        LLTempBoundListener temp(
+            pumps.obtain("reply").
+            listen("watchdog", boost::bind(&LLEventTimeout::post, boost::ref(watchdog), _1)));
+        // Now connect our target listener to the timeout filter.
+        watchdog.listen("captureReply", boost::bind(&data::captureReply, this, _1));
+        // Kick off the request...
+        reply.clear();
+        pumps.obtain("LLXMLRPCTransaction").post(request);
+        // Set the timer
+        F32 timeout(10);
+        watchdog.eventAfter(timeout, LLSD().insert("timeout", 0));
+        // and pump "mainloop" until we get something, whether from
+        // LLXMLRPCListener or from the watchdog filter.
+        LLTimer timer;
+        F32 start = timer.getElapsedTimeF32();
+        LLEventPump& mainloop(pumps.obtain("mainloop"));
+        while (reply.isUndefined())
+        {
+            mainloop.post(LLSD());
+        }
+        ensure("timeout works", (timer.getElapsedTimeF32() - start) < (timeout + 1));
+        ensure_equals(reply["responses"]["hi_there"].asString(), "Hello, world!");
+    }
+
+    template<> template<>
+    void object::test<4>()
+    {
+        set_test_name("bogus method");
+        LLSD request;
+        request["uri"] = uri;
+        request["method"] = "goodbye";
+        request["reply"] = "reply";
+        LLSD& params(request["params"]);
+        params["who"] = "world";
+        // Set up a timeout filter so we don't spin forever waiting.
+        LLEventTimeout watchdog;
+        // Connect the timeout filter to the reply pump.
+        LLTempBoundListener temp(
+            pumps.obtain("reply").
+            listen("watchdog", boost::bind(&LLEventTimeout::post, boost::ref(watchdog), _1)));
+        // Now connect our target listener to the timeout filter.
+        watchdog.listen("captureReply", boost::bind(&data::captureReply, this, _1));
+        // Kick off the request...
+        reply.clear();
+        pumps.obtain("LLXMLRPCTransaction").post(request);
+        // Set the timer
+        F32 timeout(10);
+        watchdog.eventAfter(timeout, LLSD().insert("timeout", 0));
+        // and pump "mainloop" until we get something, whether from
+        // LLXMLRPCListener or from the watchdog filter.
+        LLTimer timer;
+        F32 start = timer.getElapsedTimeF32();
+        LLEventPump& mainloop(pumps.obtain("mainloop"));
+        while (reply.isUndefined())
+        {
+            mainloop.post(LLSD());
+        }
+        ensure("timeout works", (timer.getElapsedTimeF32() - start) < (timeout + 1));
+        ensure_equals("XMLRPC error", reply["status"].asString(), "XMLRPCError");
+    }
+
+    template<> template<>
+    void object::test<5>()
+    {
+        set_test_name("bad type");
+        LLSD request;
+        request["uri"] = uri;
+        request["method"] = "getdict";
+        request["reply"] = "reply";
+        (void)request["params"];
+        // Set up a timeout filter so we don't spin forever waiting.
+        LLEventTimeout watchdog;
+        // Connect the timeout filter to the reply pump.
+        LLTempBoundListener temp(
+            pumps.obtain("reply").
+            listen("watchdog", boost::bind(&LLEventTimeout::post, boost::ref(watchdog), _1)));
+        // Now connect our target listener to the timeout filter.
+        watchdog.listen("captureReply", boost::bind(&data::captureReply, this, _1));
+        // Kick off the request...
+        reply.clear();
+        pumps.obtain("LLXMLRPCTransaction").post(request);
+        // Set the timer
+        F32 timeout(10);
+        watchdog.eventAfter(timeout, LLSD().insert("timeout", 0));
+        // and pump "mainloop" until we get something, whether from
+        // LLXMLRPCListener or from the watchdog filter.
+        LLTimer timer;
+        F32 start = timer.getElapsedTimeF32();
+        LLEventPump& mainloop(pumps.obtain("mainloop"));
+        while (reply.isUndefined())
+        {
+            mainloop.post(LLSD());
+        }
+        ensure("timeout works", (timer.getElapsedTimeF32() - start) < (timeout + 1));
+        ensure_equals(reply["status"].asString(), "BadType");
+        ensure_contains("bad type", reply["responses"]["nested_dict"].asString(), "bad XMLRPC type");
+    }
+} // namespace tut
+
+/*****************************************************************************
+*   Resolve link errors: use real machinery here, since we intend to exchange
+*   actual XML with a peer process.
+*****************************************************************************/
+// Including llxmlrpctransaction.cpp drags in the static LLXMLRPCListener
+// instantiated there. That's why it works to post requests to the LLEventPump
+// named "LLXMLRPCTransaction".
+#include "../llxmlrpctransaction.cpp"
+#include "llcontrol.cpp"
+#include "llxmltree.cpp"
+#include "llxmlparser.cpp"
diff --git a/indra/newview/tests/test_llxmlrpc_peer.py b/indra/newview/tests/test_llxmlrpc_peer.py
new file mode 100644
index 0000000000000000000000000000000000000000..cb8f7d26c4cb6dab263468bbdd0d551b0356c95c
--- /dev/null
+++ b/indra/newview/tests/test_llxmlrpc_peer.py
@@ -0,0 +1,59 @@
+#!/usr/bin/python
+"""\
+@file   test_llxmlrpc_peer.py
+@author Nat Goodspeed
+@date   2008-10-09
+@brief  This script asynchronously runs the executable (with args) specified on
+        the command line, returning its result code. While that executable is
+        running, we provide dummy local services for use by C++ tests.
+
+$LicenseInfo:firstyear=2008&license=viewergpl$
+Copyright (c) 2008, Linden Research, Inc.
+$/LicenseInfo$
+"""
+
+import os
+import sys
+from threading import Thread
+from SimpleXMLRPCServer import SimpleXMLRPCServer
+
+mydir = os.path.dirname(__file__)       # expected to be .../indra/newview/tests/
+sys.path.insert(0, os.path.join(mydir, os.pardir, os.pardir, "lib", "python"))
+sys.path.insert(1, os.path.join(mydir, os.pardir, os.pardir, "llmessage", "tests"))
+from testrunner import run, debug
+
+class TestServer(SimpleXMLRPCServer):
+    def _dispatch(self, method, params):
+        try:
+            func = getattr(self, method)
+        except AttributeError:
+            raise Exception('method "%s" is not supported' % method)
+        else:
+            # LLXMLRPCListener constructs XMLRPC parameters that arrive as a
+            # 1-tuple containing a dict.
+            return func(**(params[0]))
+
+    def hello(self, who):
+        # LLXMLRPCListener expects a dict return.
+        return {"hi_there": "Hello, %s!" % who}
+
+    def getdict(self):
+        return dict(nested_dict=dict(a=17, b=5))
+
+    def log_request(self, code, size=None):
+        # For present purposes, we don't want the request splattered onto
+        # stderr, as it would upset devs watching the test run
+        pass
+
+    def log_error(self, format, *args):
+        # Suppress error output as well
+        pass
+
+class ServerRunner(Thread):
+    def run(self):
+        server = TestServer(('127.0.0.1', 8000))
+        debug("Starting XMLRPC server...\n")
+        server.serve_forever()
+
+if __name__ == "__main__":
+    sys.exit(run(server=ServerRunner(name="xmlrpc"), *sys.argv[1:]))
diff --git a/indra/test/llevents_tut.cpp b/indra/test/llevents_tut.cpp
index e401f89b22b3e0e56c36d717f678b7117d0dd4be..31130c3c7932eb3ff08d548e3f4da6b9107803c4 100644
--- a/indra/test/llevents_tut.cpp
+++ b/indra/test/llevents_tut.cpp
@@ -32,96 +32,10 @@
 // other Linden headers
 #include "lltut.h"
 #include "stringize.h"
+#include "tests/listener.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; }
 
@@ -174,14 +88,7 @@ namespace tut
         // 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));
+        LLBoundListener connection = listener0.listenTo(per_frame);
         ensure("connected", connection.connected());
         ensure("not blocked", ! connection.blocked());
         per_frame.post(1);
@@ -207,6 +114,10 @@ namespace tut
         bool threw = false;
         try
         {
+            // 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.
             per_frame.listen(listener0.getName(), // note bug, dup name
                              boost::bind(&Listener::call, boost::ref(listener1), _1));
         }
@@ -221,8 +132,7 @@ namespace tut
         }
         ensure("threw DupListenerName", threw);
         // do it right this time
-        per_frame.listen(listener1.getName(),
-                         boost::bind(&Listener::call, boost::ref(listener1), _1));
+        listener1.listenTo(per_frame);
         per_frame.post(5);
         check_listener("got", listener0, 5);
         check_listener("got", listener1, 5);
@@ -252,16 +162,10 @@ namespace tut
         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())));
+        LLBoundListener bound0 = listener0.listenTo(per_frame, &Listener::callstop);
+        LLBoundListener bound1 = listener1.listenTo(per_frame, &Listener::call,
+                                                    // after listener0
+                                                    make<LLEventPump::NameList>(list_of(listener0.getName())));
         ensure("enabled", per_frame.enabled());
         ensure("connected 0", bound0.connected());
         ensure("unblocked 0", ! bound0.blocked());
@@ -301,7 +205,7 @@ namespace tut
         // 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.listenTo(login);
         listener0.reset(0);
         login.post(1);
         check_listener("waiting for queued event", listener0, 0);
@@ -354,11 +258,10 @@ namespace tut
     {
         set_test_name("stopListening()");
         LLEventPump& login(pumps.obtain("login"));
-        login.listen(listener0.getName(), boost::bind(&Listener::call, boost::ref(listener0), _1));
+        listener0.listenTo(login);
         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));
+        listener0.listenTo(login, &Listener::callstop);
         LLBoundListener wrong = login.getListener("bogus");
         ensure("bogus connection disconnected", ! wrong.connected());
         ensure("bogus connection blocked", wrong.blocked());
@@ -378,10 +281,8 @@ namespace tut
                         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.listenTo(filter0);
+        listener1.listenTo(filter1);
         listener0.reset(0);
         listener1.reset(0);
         upstream.post(1);
@@ -536,7 +437,7 @@ namespace tut
         // 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));
+        listener0.listenTo(random);
         eventSource("random");
         check_listener("got by pump name", listener0, 17);
         bool threw = false;
diff --git a/indra/test/llsdutil_tut.cpp b/indra/test/llsdutil_tut.cpp
index 0c4bbc2e625ad0ef75648cd234be813d9f309a3b..093a29652c698983b49484bc37259468428b4eb6 100644
--- a/indra/test/llsdutil_tut.cpp
+++ b/indra/test/llsdutil_tut.cpp
@@ -44,12 +44,40 @@
 #include "v4math.h"
 #include "llquaternion.h"
 #include "llsdutil.h"
-
+#include <set>
+#include <boost/range.hpp>
 
 namespace tut
 {
 	struct llsdutil_data
 	{
+        void test_matches(const std::string& proto_key, const LLSD& possibles,
+                          const char** begin, const char** end)
+        {
+            std::set<std::string> succeed(begin, end);
+            LLSD prototype(possibles[proto_key]);
+            for (LLSD::map_const_iterator pi(possibles.beginMap()), pend(possibles.endMap());
+                 pi != pend; ++pi)
+            {
+                std::string match(llsd_matches(prototype, pi->second));
+                std::set<std::string>::const_iterator found = succeed.find(pi->first);
+                if (found != succeed.end())
+                {
+                    // This test is supposed to succeed. Comparing to the
+                    // empty string ensures that if the test fails, it will
+                    // display the string received so we can tell what failed.
+                    ensure_equals("match", match, "");
+                }
+                else
+                {
+                    // This test is supposed to fail. If we get a false match,
+                    // the string 'match' will be empty, which doesn't tell us
+                    // much about which case went awry. So construct a more
+                    // detailed description string.
+                    ensure(proto_key + " shouldn't match " + pi->first, ! match.empty());
+                }
+            }
+        }
 	};
 	typedef test_group<llsdutil_data> llsdutil_test;;
 	typedef llsdutil_test::object llsdutil_object;
@@ -159,4 +187,154 @@ namespace tut
 		LLSD sd1 = ll_sd_from_color4(c1);
 		ensure_equals("sd -> LLColor4 -> sd", sd, sd1);
 	}
+
+    template<> template<>
+    void llsdutil_object::test<9>()
+    {
+        set_test_name("llsd_matches");
+
+        // for this test, construct a map of all possible LLSD types
+        LLSD map;
+        map.insert("empty",     LLSD());
+        map.insert("Boolean",   LLSD::Boolean());
+        map.insert("Integer",   LLSD::Integer(0));
+        map.insert("Real",      LLSD::Real(0.0));
+        map.insert("String",    LLSD::String("bah"));
+        map.insert("NumString", LLSD::String("1"));
+        map.insert("UUID",      LLSD::UUID());
+        map.insert("Date",      LLSD::Date());
+        map.insert("URI",       LLSD::URI());
+        map.insert("Binary",    LLSD::Binary());
+        map.insert("Map",       LLSD().insert("foo", LLSD()));
+        // array can't be constructed on the fly
+        LLSD array;
+        array.append(LLSD());
+        map.insert("Array",     array);
+
+        // These iterators are declared outside our various for loops to avoid
+        // fatal MSVC warning: "I used to be broken, but I'm all better now!"
+        LLSD::map_const_iterator mi(map.beginMap()), mend(map.endMap());
+
+        // empty prototype matches anything
+        for (mi = map.beginMap(); mi != mend; ++mi)
+        {
+            ensure_equals(std::string("empty matches ") + mi->first, llsd_matches(LLSD(), mi->second), "");
+        }
+
+        LLSD proto_array, data_array;
+        for (int i = 0; i < 3; ++i)
+        {
+            proto_array.append(LLSD());
+            data_array.append(LLSD());
+        }
+
+        // prototype array matches only array
+        for (mi = map.beginMap(); mi != mend; ++mi)
+        {
+            ensure(std::string("array doesn't match ") + mi->first,
+                   ! llsd_matches(proto_array, mi->second).empty());
+        }
+
+        // data array must be at least as long as prototype array
+        proto_array.append(LLSD());
+        ensure_equals("data array too short", llsd_matches(proto_array, data_array),
+                      "Array size 4 required instead of Array size 3");
+        data_array.append(LLSD());
+        ensure_equals("data array just right", llsd_matches(proto_array, data_array), "");
+        data_array.append(LLSD());
+        ensure_equals("data array longer", llsd_matches(proto_array, data_array), "");
+
+        // array element matching
+        data_array[0] = LLSD::String();
+        ensure_equals("undefined prototype array entry", llsd_matches(proto_array, data_array), "");
+        proto_array[0] = LLSD::Binary();
+        ensure_equals("scalar prototype array entry", llsd_matches(proto_array, data_array),
+                      "[0]: Binary required instead of String");
+        data_array[0] = LLSD::Binary();
+        ensure_equals("matching prototype array entry", llsd_matches(proto_array, data_array), "");
+
+        // build a coupla maps
+        LLSD proto_map, data_map;
+        data_map["got"] = LLSD();
+        data_map["found"] = LLSD();
+        for (LLSD::map_const_iterator dmi(data_map.beginMap()), dmend(data_map.endMap());
+             dmi != dmend; ++dmi)
+        {
+            proto_map[dmi->first] = dmi->second;
+        }
+        proto_map["foo"] = LLSD();
+        proto_map["bar"] = LLSD();
+
+        // prototype map matches only map
+        for (mi = map.beginMap(); mi != mend; ++mi)
+        {
+            ensure(std::string("map doesn't match ") + mi->first,
+                   ! llsd_matches(proto_map, mi->second).empty());
+        }
+
+        // data map must contain all keys in prototype map
+        std::string error(llsd_matches(proto_map, data_map));
+        ensure_contains("missing keys", error, "missing keys");
+        ensure_contains("missing foo", error, "foo");
+        ensure_contains("missing bar", error, "bar");
+        ensure_does_not_contain("found found", error, "found");
+        ensure_does_not_contain("got got", error, "got");
+        data_map["bar"] = LLSD();
+        error = llsd_matches(proto_map, data_map);
+        ensure_contains("missing foo", error, "foo");
+        ensure_does_not_contain("got bar", error, "bar");
+        data_map["foo"] = LLSD();
+        ensure_equals("data map just right", llsd_matches(proto_map, data_map), "");
+        data_map["extra"] = LLSD();
+        ensure_equals("data map with extra", llsd_matches(proto_map, data_map), "");
+
+        // map element matching
+        data_map["foo"] = LLSD::String();
+        ensure_equals("undefined prototype map entry", llsd_matches(proto_map, data_map), "");
+        proto_map["foo"] = LLSD::Binary();
+        ensure_equals("scalar prototype map entry", llsd_matches(proto_map, data_map),
+                      "['foo']: Binary required instead of String");
+        data_map["foo"] = LLSD::Binary();
+        ensure_equals("matching prototype map entry", llsd_matches(proto_map, data_map), "");
+
+        // String
+        {
+            static const char* matches[] = { "String", "NumString", "Boolean", "Integer",
+                                             "Real", "UUID", "Date", "URI" };
+            test_matches("String", map, boost::begin(matches), boost::end(matches));
+        }
+
+        // Boolean, Integer, Real
+        static const char* numerics[] = { "Boolean", "Integer", "Real" };
+        for (const char **ni = boost::begin(numerics), **nend = boost::end(numerics);
+             ni != nend; ++ni)
+        {
+            static const char* matches[] = { "Boolean", "Integer", "Real", "String", "NumString" };
+            test_matches(*ni, map, boost::begin(matches), boost::end(matches));
+        }
+
+        // UUID
+        {
+            static const char* matches[] = { "UUID", "String", "NumString" };
+            test_matches("UUID", map, boost::begin(matches), boost::end(matches));
+        }
+
+        // Date
+        {
+            static const char* matches[] = { "Date", "String", "NumString" };
+            test_matches("Date", map, boost::begin(matches), boost::end(matches));
+        }
+
+        // URI
+        {
+            static const char* matches[] = { "URI", "String", "NumString" };
+            test_matches("URI", map, boost::begin(matches), boost::end(matches));
+        }
+
+        // Binary
+        {
+            static const char* matches[] = { "Binary" };
+            test_matches("Binary", map, boost::begin(matches), boost::end(matches));
+        }
+    }
 }
diff --git a/indra/test/lltut.cpp b/indra/test/lltut.cpp
index 201e174f9cf608289e75ee9c286a5adb1b8eb988..e4e0de1ff1d7696f13a46bf63535edc3114a0da3 100644
--- a/indra/test/lltut.cpp
+++ b/indra/test/lltut.cpp
@@ -76,9 +76,13 @@ namespace tut
 
 	void ensure_equals(const char* m, const LLSD& actual,
 		const LLSD& expected)
+    {
+        ensure_equals(std::string(m), actual, expected);
+    }
+
+	void ensure_equals(const std::string& msg, const LLSD& actual,
+		const LLSD& expected)
 	{
-		const std::string& msg = m ? m : "";
-		
 		ensure_equals(msg + " type", actual.type(), expected.type());
 		switch (actual.type())
 		{
@@ -128,7 +132,7 @@ namespace tut
 				{
 					ensure_equals(msg + " map keys", 
 						actual_iter->first, expected_iter->first);
-					ensure_equals((msg + "[" + actual_iter->first + "]").c_str(),
+					ensure_equals(msg + "[" + actual_iter->first + "]",
 						actual_iter->second, expected_iter->second);
 					++actual_iter;
 					++expected_iter;
@@ -141,7 +145,7 @@ namespace tut
 				
 				for(int i = 0; i < actual.size(); ++i)
 				{
-					ensure_equals((msg + llformat("[%d]", i)).c_str(),
+					ensure_equals(msg + llformat("[%d]", i),
 						actual[i], expected[i]);
 				}
 				return;
diff --git a/indra/test/lltut.h b/indra/test/lltut.h
index 47ea9d3f9e27a55472fb4cfb6a642c4f11aa2fdf..ba3791cbd4e5b910e5856a0e0ba9877e470fbc4e 100644
--- a/indra/test/lltut.h
+++ b/indra/test/lltut.h
@@ -121,6 +121,9 @@ namespace tut
 
 	void ensure_equals(const char* msg,
 		const LLSD& actual, const LLSD& expected);
+
+	void ensure_equals(const std::string& msg,
+		const LLSD& actual, const LLSD& expected);
 	
 	void ensure_starts_with(const std::string& msg,
 		const std::string& actual, const std::string& expectedStart);
diff --git a/indra/test/test.cpp b/indra/test/test.cpp
index ba81c6e49e96067384fd12803b79bea39c4c07d4..0ba5758e1506b7fbe392805d00f2266730941b88 100644
--- a/indra/test/test.cpp
+++ b/indra/test/test.cpp
@@ -64,13 +64,14 @@ namespace tut
 class LLTestCallback : public tut::callback
 {
 public:
-	LLTestCallback(bool verbose_mode, std::ostream *stream) :
+	LLTestCallback(bool verbose_mode, std::ostream *stream, bool wait) :
 		mVerboseMode(verbose_mode),
 		mTotalTests(0),
 		mPassedTests(0),
 		mFailedTests(0),
 		mSkippedTests(0),
-		mStream(stream)
+		mStream(stream),
+        mWaitAtExit(wait)
 	{
 	}
 
@@ -137,6 +138,11 @@ public:
 		}
 		run_completed_(std::cout);
 
+        if(mWaitAtExit) {
+            std::cerr << "Waiting for input before exiting..." << std::endl;
+	        std::cin.get();
+        }
+
 		if (mFailedTests > 0)
 		{
 			exit(1);
@@ -176,6 +182,7 @@ protected:
 	int mFailedTests;
 	int mSkippedTests;
 	std::ostream *mStream;
+    bool mWaitAtExit;
 };
 
 static const apr_getopt_option_t TEST_CL_OPTIONS[] =
@@ -328,7 +335,7 @@ int main(int argc, char **argv)
 	}
 
 	// run the tests
-	LLTestCallback callback(verbose_mode, output);
+	LLTestCallback callback(verbose_mode, output, wait_at_exit);
 	tut::runner.get().set_callback(&callback);
 	
 	if(test_group.empty())
@@ -339,12 +346,6 @@ int main(int argc, char **argv)
 	{
 		tut::runner.get().run_tests(test_group);
 	}
-
-	if (wait_at_exit)
-	{
-		std::cerr << "Waiting for input before exiting..." << std::endl;
-		std::cin.get();
-	}
 	
 	if (output)
 	{
diff --git a/indra/viewer_components/CMakeLists.txt b/indra/viewer_components/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d5eea0d0b0010e5b9aa5f579566b7f29b770cd40
--- /dev/null
+++ b/indra/viewer_components/CMakeLists.txt
@@ -0,0 +1 @@
+add_subdirectory(login)
diff --git a/indra/viewer_components/login/CMakeLists.txt b/indra/viewer_components/login/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..434b58f5c7172f8692f3c283cd47a3f4b72703ca
--- /dev/null
+++ b/indra/viewer_components/login/CMakeLists.txt
@@ -0,0 +1,43 @@
+project(login)
+
+include(00-Common)
+include(LLCommon)
+include(LLMath)
+include(LLXML)
+include(Pth)
+
+include_directories(
+    ${LLCOMMON_INCLUDE_DIRS}
+    ${LLMATH_INCLUDE_DIRS}
+    ${LLXML_INCLUDE_DIRS}
+    ${PTH_INCLUDE_DIRS}
+    )
+
+set(login_SOURCE_FILES
+    lllogin.cpp
+    )
+
+set(login_HEADER_FILES
+    lllogin.h
+    )
+
+set_source_files_properties(${login_HEADER_FILES}
+                            PROPERTIES HEADER_FILE_ONLY TRUE)
+
+list(APPEND 
+    login_SOURCE_FILES 
+    ${login_HEADER_FILES} 
+    )
+
+add_library(lllogin 
+            ${login_SOURCE_FILES}
+            )
+
+target_link_libraries(lllogin
+    ${LLCOMMON_LIBRARIES}
+    ${LLMATH_LIBRARIES}
+    ${LLXML_LIBRARIES}
+    ${PTH_LIBRARIES}
+    )
+
+ADD_BUILD_TEST(lllogin lllogin "")
diff --git a/indra/viewer_components/login/lllogin.cpp b/indra/viewer_components/login/lllogin.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7f2b27e64c8c34ae3119e99285728415225d53af
--- /dev/null
+++ b/indra/viewer_components/login/lllogin.cpp
@@ -0,0 +1,383 @@
+/** 
+ * @file lllogin.cpp
+ *
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * 
+ * Copyright (c) 2009, Linden Research, Inc.
+ * 
+ * Second Life Viewer Source Code
+ * The source code in this file ("Source Code") is provided by Linden Lab
+ * to you under the terms of the GNU General Public License, version 2.0
+ * ("GPL"), unless you have obtained a separate licensing agreement
+ * ("Other License"), formally executed by you and Linden Lab.  Terms of
+ * the GPL can be found in doc/GPL-license.txt in this distribution, or
+ * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
+ * 
+ * There are special exceptions to the terms and conditions of the GPL as
+ * it is applied to this Source Code. View the full text of the exception
+ * in the file doc/FLOSS-exception.txt in this software distribution, or
+ * online at
+ * http://secondlifegrid.net/programs/open_source/licensing/flossexception
+ * 
+ * By copying, modifying or distributing this software, you acknowledge
+ * that you have read and understood your obligations described above,
+ * and agree to abide by those obligations.
+ * 
+ * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
+ * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
+ * COMPLETENESS OR PERFORMANCE.
+ * $/LicenseInfo$
+ */
+
+#include <boost/coroutine/coroutine.hpp>
+#include "linden_common.h"
+#include "llsd.h"
+#include "llsdutil.h"
+
+/*==========================================================================*|
+#ifdef LL_WINDOWS
+	// non-virtual destructor warning, boost::statechart does this intentionally.
+	#pragma warning (disable : 4265) 
+#endif
+|*==========================================================================*/
+
+#include "lllogin.h"
+
+#include <boost/bind.hpp>
+#include <boost/scoped_ptr.hpp>
+
+#include "llevents.h"
+#include "lleventfilter.h"
+#include "lleventcoro.h"
+
+//*********************
+// LLLogin
+// *NOTE:Mani - Is this Impl needed now that the state machine runs the show?
+class LLLogin::Impl
+{
+public:
+    Impl():
+		mPump("login", true) // Create the module's event pump with a tweaked (unique) name.
+    {
+        mValidAuthResponse["status"]        = LLSD();
+        mValidAuthResponse["errorcode"]     = LLSD();
+        mValidAuthResponse["error"]         = LLSD();
+        mValidAuthResponse["transfer_rate"] = LLSD();
+    }
+
+    void connect(const std::string& uri, const LLSD& credentials);
+    void disconnect();
+	LLEventPump& getEventPump() { return mPump; }
+
+private:
+	void sendProgressEvent(const std::string& desc, const LLSD& data = LLSD::emptyMap())
+	{
+		LLSD status_data;
+		status_data["state"] = desc;
+		status_data["progress"] = 0.0f;
+
+		if(mAuthResponse.has("transfer_rate"))
+		{
+			status_data["transfer_rate"] = mAuthResponse["transfer_rate"];
+		}
+
+		if(data.size() != 0)
+		{
+			status_data["data"] = data;
+		}
+
+		mPump.post(status_data);
+	}
+
+    LLSD validateResponse(const std::string& pumpName, const LLSD& response)
+    {
+        // Validate the response. If we don't recognize it, things
+        // could get ugly.
+        std::string mismatch(llsd_matches(mValidAuthResponse, response));
+        if (! mismatch.empty())
+        {
+            LL_ERRS("LLLogin") << "Received unrecognized event (" << mismatch << ") on "
+                               << pumpName << "pump: " << response
+                               << LL_ENDL;
+            return LLSD();
+        }
+
+        return response;
+    }
+
+    typedef boost::coroutines::coroutine<void(const std::string&, const LLSD&)> coroutine_type;
+
+    void login_(coroutine_type::self& self, const std::string& uri, const LLSD& credentials);
+
+    boost::scoped_ptr<coroutine_type> mCoro;
+    LLEventStream mPump;
+	LLSD mAuthResponse, mValidAuthResponse;
+};
+
+void LLLogin::Impl::connect(const std::string& uri, const LLSD& credentials)
+{
+    // If there's a previous coroutine instance, and that instance is still
+    // active, destroying the instance will terminate the coroutine by
+    // throwing an exception, thus unwinding the stack and destroying all
+    // local objects. It should (!) all Just Work. Nonetheless, it would be
+    // strange, so make a note of it.
+    if (mCoro && *mCoro)
+    {
+        LL_WARNS("LLLogin") << "Previous login attempt interrupted by new request" << LL_ENDL;
+    }
+
+    // Construct a coroutine that will run our login_() method; placeholders
+    // forward the params from the (*mCoro)(etc.) call below. Using scoped_ptr
+    // ensures that if mCoro was already pointing to a previous instance, that
+    // old instance will be destroyed as noted above.
+    mCoro.reset(new coroutine_type(boost::bind(&Impl::login_, this, _1, _2, _3)));
+    // Run the coroutine until its first wait; at that point, return here.
+    (*mCoro)(std::nothrow, uri, credentials);
+    std::cout << "Here I am\n";
+}
+
+void LLLogin::Impl::login_(coroutine_type::self& self,
+                           const std::string& uri, const LLSD& credentials)
+{
+    // Mimicking previous behavior, every time the OldSchoolLogin state
+    // machine arrived in the Offline state, it would send a progress
+    // announcement.
+    sendProgressEvent("offline", mAuthResponse["responses"]);
+    // Arriving in SRVRequest state
+    LLEventStream replyPump("reply", true);
+    // Should be an array of one or more uri strings.
+    LLSD rewrittenURIs;
+    {
+        LLEventTimeout filter(replyPump);
+        sendProgressEvent("srvrequest");
+
+        // Request SRV record.
+        LL_INFOS("LLLogin") << "Requesting SRV record from " << uri << LL_ENDL;
+
+        // *NOTE:Mani - Completely arbitrary timeout value for SRV request.
+        filter.errorAfter(5, "SRV Request timed out!");
+
+        // Make request
+        LLSD request;
+        request["op"] = "rewriteURI";
+        request["uri"] = uri;
+        request["reply"] = replyPump.getName();
+        rewrittenURIs = postAndWait(self, request, "LLAres", filter);
+    } // we no longer need the filter
+
+    LLEventPump& xmlrpcPump(LLEventPumps::instance().obtain("LLXMLRPCTransaction"));
+
+    // Loop through the rewrittenURIs, counting attempts along the way.
+    // Because of possible redirect responses, we may make more than one
+    // attempt per rewrittenURIs entry.
+    LLSD::Integer attempts = 0;
+    for (LLSD::array_const_iterator urit(rewrittenURIs.beginArray()),
+             urend(rewrittenURIs.endArray());
+         urit != urend; ++urit)
+    {
+        LLSD request(credentials);
+        request["reply"] = replyPump.getName();
+        request["uri"] = *urit;
+        std::string status;
+
+        // Loop back to here if login attempt redirects to a different
+        // request["uri"]
+        for (;;)
+        {
+            ++attempts;
+            LLSD progress_data;
+            progress_data["attempt"] = attempts;
+            progress_data["request"] = request;
+            sendProgressEvent("authenticating", progress_data);
+
+            // We expect zero or more "Downloading" status events, followed by
+            // exactly one event with some other status. Use postAndWait() the
+            // first time, because -- at least in unit-test land -- it's
+            // possible for the reply to arrive before the post() call
+            // returns. Subsequent responses, of course, must be awaited
+            // without posting again.
+            for (mAuthResponse = validateResponse(replyPump.getName(),
+                                     postAndWait(self, request, xmlrpcPump, replyPump, "reply"));
+                 mAuthResponse["status"].asString() == "Downloading";
+                 mAuthResponse = validateResponse(replyPump.getName(),
+                                     waitForEventOn(self, replyPump)))
+            {
+                // Still Downloading -- send progress update.
+                sendProgressEvent("downloading");
+            }
+            status = mAuthResponse["status"].asString();
+
+            // Okay, we've received our final status event for this
+            // request. Unless we got a redirect response, break the retry
+            // loop for the current rewrittenURIs entry.
+            if (! (status == "Complete" &&
+                   mAuthResponse["responses"]["login"].asString() == "indeterminate"))
+            {
+                break;
+            }
+
+            // Here the login service at the current URI is redirecting us
+            // to some other URI ("indeterminate" -- why not "redirect"?).
+            // The response should contain another uri to try, with its
+            // own auth method.
+            request["uri"] = mAuthResponse["next_url"];
+            request["method"] = mAuthResponse["next_method"];
+        } // loop back to try the redirected URI
+
+        // Here we're done with redirects for the current rewrittenURIs
+        // entry.
+        if (status == "Complete")
+        {
+            // StatusComplete does not imply auth success. Check the
+            // actual outcome of the request. We've already handled the
+            // "indeterminate" case in the loop above.
+            sendProgressEvent((mAuthResponse["responses"]["login"].asString() == "true")?
+                              "online" : "offline",
+                              mAuthResponse["responses"]);
+            return;             // Done!
+        }
+        // If we don't recognize status at all, trouble
+        if (! (status == "CURLError"
+               || status == "XMLRPCError"
+               || status == "OtherError"))
+        {
+            LL_ERRS("LLLogin") << "Unexpected status from " << xmlrpcPump.getName() << " pump: "
+                               << mAuthResponse << LL_ENDL;
+            return;
+        }
+
+        // Here status IS one of the errors tested above.
+    } // Retry if there are any more rewrittenURIs.
+
+    // Here we got through all the rewrittenURIs without succeeding. Tell
+    // caller this didn't work out so well. Of course, the only failure data
+    // we can reasonably show are from the last of the rewrittenURIs.
+    sendProgressEvent("offline", mAuthResponse["responses"]);
+}
+
+void LLLogin::Impl::disconnect()
+{
+    sendProgressEvent("offline", mAuthResponse["responses"]);
+}
+
+//*********************
+// LLLogin
+LLLogin::LLLogin() :
+	mImpl(new LLLogin::Impl())
+{
+}
+
+LLLogin::~LLLogin()
+{
+}
+
+void LLLogin::connect(const std::string& uri, const LLSD& credentials)
+{
+	mImpl->connect(uri, credentials);
+}
+
+
+void LLLogin::disconnect()
+{
+	mImpl->disconnect();
+}
+
+LLEventPump& LLLogin::getEventPump()
+{
+	return mImpl->getEventPump();
+}
+
+// The following is the list of important functions that happen in the 
+// current login process that we want to move to this login module.
+
+// The list associates to event with the original idle_startup() 'STATE'.
+
+// Rewrite URIs
+ // State_LOGIN_AUTH_INIT
+// Given a vector of login uris (usually just one), perform a dns lookup for the 
+// SRV record from each URI. I think this is used to distribute login requests to 
+// a single URI to multiple hosts.
+// This is currently a synchronous action. (See LLSRV::rewriteURI() implementation)
+// On dns lookup error the output uris == the input uris.
+//
+// Input: A vector of login uris
+// Output: A vector of login uris
+//
+// Code:
+// std::vector<std::string> uris;
+// LLViewerLogin::getInstance()->getLoginURIs(uris);
+// std::vector<std::string>::const_iterator iter, end;
+// for (iter = uris.begin(), end = uris.end(); iter != end; ++iter)
+// {
+//	std::vector<std::string> rewritten;
+//	rewritten = LLSRV::rewriteURI(*iter);
+//	sAuthUris.insert(sAuthUris.end(),
+//					 rewritten.begin(), rewritten.end());
+// }
+// sAuthUriNum = 0;
+
+// Authenticate 
+// STATE_LOGIN_AUTHENTICATE
+// Connect to the login server, presumably login.cgi, requesting the login 
+// and a slew of related initial connection information.
+// This is an asynch action. The final response, whether success or error
+// is handled by STATE_LOGIN_PROCESS_REPONSE.
+// There is no immediate error or output from this call.
+// 
+// Input: 
+//  URI
+//  Credentials (first, last, password)
+//  Start location
+//  Bool Flags:
+//    skip optional update
+//    accept terms of service
+//    accept critical message
+//  Last exec event. (crash state of previous session)
+//  requested optional data (inventory skel, initial outfit, etc.)
+//  local mac address
+//  viewer serial no. (md5 checksum?)
+
+//sAuthUriNum = llclamp(sAuthUriNum, 0, (S32)sAuthUris.size()-1);
+//LLUserAuth::getInstance()->authenticate(
+//	sAuthUris[sAuthUriNum],
+//	auth_method,
+//	firstname,
+//	lastname,			
+//	password, // web_login_key,
+//	start.str(),
+//	gSkipOptionalUpdate,
+//	gAcceptTOS,
+//	gAcceptCriticalMessage,
+//	gLastExecEvent,
+//	requested_options,
+//	hashed_mac_string,
+//	LLAppViewer::instance()->getSerialNumber());
+
+//
+// Download the Response
+// STATE_LOGIN_NO_REPONSE_YET and STATE_LOGIN_DOWNLOADING
+// I had assumed that this was default behavior of the message system. However...
+// During login, the message system is checked only by these two states in idle_startup().
+// I guess this avoids the overhead of checking network messages for those login states
+// that don't need to do so, but geez!
+// There are two states to do this one function just to update the login
+// status text from 'Logging In...' to 'Downloading...'
+// 
+
+//
+// Handle Login Response
+// STATE_LOGIN_PROCESS_RESPONSE
+// 
+// This state handle the result of the request to login. There is a metric ton of
+// code in this case. This state will transition to:
+// STATE_WORLD_INIT, on success.
+// STATE_AUTHENTICATE, on failure.
+// STATE_UPDATE_CHECK, to handle user during login interaction like TOS display.
+//
+// Much of the code in this case belongs on the viewer side of the fence and not in login. 
+// Login should probably return with a couple of events, success and failure.
+// Failure conditions can be specified in the events data pacet to allow the viewer 
+// to re-engauge login as is appropriate. (Or should there be multiple failure messages?)
+// Success is returned with the data requested from the login. According to OGP specs 
+// there may be intermediate steps before reaching this result in future login 
+// implementations.
diff --git a/indra/viewer_components/login/lllogin.h b/indra/viewer_components/login/lllogin.h
new file mode 100644
index 0000000000000000000000000000000000000000..0598b4e45724c3386b6bfb053e29ef966b0e34bc
--- /dev/null
+++ b/indra/viewer_components/login/lllogin.h
@@ -0,0 +1,133 @@
+/** 
+ * @file lllogin.h
+ *
+ * $LicenseInfo:firstyear=2009&license=viewergpl$
+ * 
+ * Copyright (c) 2009, Linden Research, Inc.
+ * 
+ * Second Life Viewer Source Code
+ * The source code in this file ("Source Code") is provided by Linden Lab
+ * to you under the terms of the GNU General Public License, version 2.0
+ * ("GPL"), unless you have obtained a separate licensing agreement
+ * ("Other License"), formally executed by you and Linden Lab.  Terms of
+ * the GPL can be found in doc/GPL-license.txt in this distribution, or
+ * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
+ * 
+ * There are special exceptions to the terms and conditions of the GPL as
+ * it is applied to this Source Code. View the full text of the exception
+ * in the file doc/FLOSS-exception.txt in this software distribution, or
+ * online at
+ * http://secondlifegrid.net/programs/open_source/licensing/flossexception
+ * 
+ * By copying, modifying or distributing this software, you acknowledge
+ * that you have read and understood your obligations described above,
+ * and agree to abide by those obligations.
+ * 
+ * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
+ * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
+ * COMPLETENESS OR PERFORMANCE.
+ * $/LicenseInfo$
+ */
+
+#ifndef LL_LLLOGIN_H
+#define LL_LLLOGIN_H
+
+#include <boost/scoped_ptr.hpp>
+
+class LLSD;
+class LLEventPump;
+
+/**
+ * @class LLLogin
+ * @brief Class to encapsulate the action and state of grid login.
+ */
+class LLLogin
+{
+public:
+	LLLogin();
+	~LLLogin();
+
+	/** 
+	 * Make a connection to a grid.
+	 * @param uri The 'well known and published' authentication URL.
+	 * @param credentials LLSD data that contians the credentials.
+	 * *NOTE:Mani The credential data can vary depending upon the authentication
+	 * method used. The current interface matches the values passed to
+	 * the XMLRPC login request.
+	 {
+		method			:	string, 
+		first			:	string,
+		last			:	string,
+		passwd			:	string,
+		start			:	string,
+		skipoptional	:	bool,
+		agree_to_tos	:	bool,
+		read_critical	:	bool,
+		last_exec_event	:	int,
+		version			:	string,
+		channel			:	string,
+		mac				:	string,
+		id0				:	string,
+		options			:   [ strings ]
+	 }
+	 
+	 */
+	void connect(const std::string& uri, const LLSD& credentials);
+	
+    /** 
+	 * Disconnect from a the current connection.
+	 */
+	void disconnect();
+
+    /** 
+	 * Retrieve the event pump from this login class.
+	 */
+	LLEventPump& getEventPump();
+
+	/*
+	Event API
+
+	LLLogin will issue multiple events to it pump to indicate the 
+	progression of states through login. The most important 
+	states are "offline" and "online" which indicate auth failure 
+	and auth success respectively.
+
+	pump: login (tweaked)
+	These are the events posted to the 'login' 
+	event pump from the login module.
+	{
+		state		:	string, // See below for the list of states.
+		progress	:   real // for progress bar.
+		data		:   LLSD // Dependent upon state.
+	}
+	
+	States for method 'login_to_simulator'
+	offline - set initially state and upon failure. data is the server response.
+	srvrequest - upon uri rewrite request. no data.
+	authenticating - upon auth request. data, 'attempt' number and 'request' llsd.
+	downloading - upon ack from auth server, before completion. no data
+	online - upon auth success. data is server response.
+
+
+	Dependencies:
+	pump: LLAres 
+	LLLogin makes a request for a SRV record from the uri provided by the connect method.
+	The following event pump should exist to service that request.
+	pump name: LLAres
+	request = {
+		op : "rewriteURI"
+		uri : string
+		reply : string
+
+	pump: LLXMLRPCListener
+	The request merely passes the credentials LLSD along, with one additional 
+	member, 'reply', which is the string name of the event pump to reply on. 
+	
+	*/
+
+private:
+	class Impl;
+	boost::scoped_ptr<Impl> mImpl;
+};
+
+#endif // LL_LLLOGIN_H
diff --git a/indra/viewer_components/login/tests/lllogin_test.cpp b/indra/viewer_components/login/tests/lllogin_test.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..07c9db109953d8490800f3e5843fb6290a40651d
--- /dev/null
+++ b/indra/viewer_components/login/tests/lllogin_test.cpp
@@ -0,0 +1,382 @@
+/**
+ * @file   llviewerlogin_test.cpp
+ * @author Mark Palange
+ * @date   2009-02-26
+ * @brief  Tests of lllazy.h.
+ * 
+ * $LicenseInfo:firstyear=2009&license=internal$
+ * Copyright (c) 2009, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "../lllogin.h"
+// STL headers
+// std headers
+#include <iostream>
+// external library headers
+// other Linden headers
+#include "llsd.h"
+#include "../../../test/lltut.h"
+#include "llevents.h"
+
+/*****************************************************************************
+*   TUT
+*****************************************************************************/
+// This is a listener to receive results from lllogin.
+class LoginListener
+{
+	std::string mName;
+	LLSD mLastEvent;
+public:
+	LoginListener(const std::string& name) : 
+		mName(name)
+	{}
+
+	bool call(const LLSD& event)
+	{
+		std::cout << "LoginListener called!: " << event << std::endl;
+		mLastEvent = event;
+		return false;
+	}
+
+    LLBoundListener listenTo(LLEventPump& pump)
+    {
+        return pump.listen(mName, boost::bind(&LoginListener::call, this, _1));
+	}
+
+	const LLSD& lastEvent() { return mLastEvent; }
+};
+
+class LLAresListener
+{
+	std::string mName;
+	LLSD mEvent;
+	bool mImmediateResponse;
+	bool mMultipleURIResponse;
+	
+public:
+	LLAresListener(const std::string& name, 
+				   bool i = false,
+				   bool m = false
+				   ) : 
+		mName(name),
+		mImmediateResponse(i),
+		mMultipleURIResponse(m)
+	{}
+
+	bool handle_event(const LLSD& event)
+	{
+		std::cout << "LLAresListener called!: " << event << std::endl;
+		mEvent = event;
+		if(mImmediateResponse)
+		{
+			sendReply();
+		}
+		return false;
+	}
+
+	void sendReply()
+	{
+		if(mEvent["op"].asString() == "rewriteURI")
+		{
+			LLSD result;
+			if(mMultipleURIResponse)
+			{
+				result.append(LLSD("login.foo.com"));
+			}
+			result.append(mEvent["uri"]);
+			LLEventPumps::instance().obtain(mEvent["reply"]).post(result);
+		}
+	}
+
+	LLBoundListener listenTo(LLEventPump& pump)
+    {
+        return pump.listen(mName, boost::bind(&LLAresListener::handle_event, this, _1));
+	}
+};
+
+class LLXMLRPCListener
+{
+	std::string mName;
+	LLSD mEvent;
+	bool mImmediateResponse;
+	LLSD mResponse;
+
+public:
+	LLXMLRPCListener(const std::string& name, 
+					 bool i = false,
+					 const LLSD& response = LLSD()
+					 ) : 
+		mName(name),
+		mImmediateResponse(i),
+		mResponse(response)
+	{
+		if(mResponse.isUndefined())
+		{
+			mResponse["status"] = "Complete"; // StatusComplete
+			mResponse["errorcode"] = 0;
+			mResponse["error"] = "dummy response";
+			mResponse["transfer_rate"] = 0;
+			mResponse["responses"]["login"] = true;
+		}
+	}
+
+	void setResponse(const LLSD& r) 
+	{ 
+		mResponse = r; 
+	}
+
+	bool handle_event(const LLSD& event)
+	{
+		std::cout << "LLXMLRPCListener called!: " << event << std::endl;
+		mEvent = event;
+		if(mImmediateResponse)
+		{
+			sendReply();
+		}
+		return false;
+	}
+
+	void sendReply()
+	{
+		LLEventPumps::instance().obtain(mEvent["reply"]).post(mResponse);
+	}
+
+	LLBoundListener listenTo(LLEventPump& pump)
+    {
+        return pump.listen(mName, boost::bind(&LLXMLRPCListener::handle_event, this, _1));
+	}
+};
+
+namespace tut
+{
+    struct llviewerlogin_data
+    {
+		llviewerlogin_data() :
+            pumps(LLEventPumps::instance())
+		{}
+		LLEventPumps& pumps;
+	};
+
+    typedef test_group<llviewerlogin_data> llviewerlogin_group;
+    typedef llviewerlogin_group::object llviewerlogin_object;
+    llviewerlogin_group llviewerlogingrp("llviewerlogin");
+
+    template<> template<>
+    void llviewerlogin_object::test<1>()
+    {
+		// Testing login with immediate repsonses from Ares and XMLPRC
+		// The response from both requests will come before the post request exits.
+		// This tests an edge case of the login state handling.
+		LLEventStream llaresPump("LLAres"); // Dummy LLAres pump.
+		LLEventStream xmlrpcPump("LLXMLRPCTransaction"); // Dummy XMLRPC pump
+
+		bool respond_immediately = true;
+		// Have 'dummy ares' repsond immediately. 
+		LLAresListener dummyLLAres("dummy_llares", respond_immediately);
+		dummyLLAres.listenTo(llaresPump);
+
+		// Have dummy XMLRPC respond immediately.
+		LLXMLRPCListener dummyXMLRPC("dummy_xmlrpc", respond_immediately);
+		dummyXMLRPC.listenTo(xmlrpcPump);
+
+		LLLogin login;
+
+		LoginListener listener("test_ear");
+		listener.listenTo(login.getEventPump());
+
+		LLSD credentials;
+		credentials["first"] = "foo";
+		credentials["last"] = "bar";
+		credentials["passwd"] = "secret";
+
+		login.connect("login.bar.com", credentials);
+
+		ensure_equals("Online state", listener.lastEvent()["state"].asString(), "online");
+	}
+
+    template<> template<>
+    void llviewerlogin_object::test<2>()
+    {
+		// Tests a successful login in with delayed responses. 
+		// Also includes 'failure' that cause the login module
+		// To re-attempt connection, once from a basic failure
+		// and once from the 'indeterminate' response.
+
+		set_test_name("LLLogin multiple srv uris w/ success");
+
+		// Testing normal login procedure.
+		LLEventStream llaresPump("LLAres"); // Dummy LLAres pump.
+		LLEventStream xmlrpcPump("LLXMLRPCTransaction"); // Dummy XMLRPC pump
+
+		bool respond_immediately = false;
+		bool multiple_addresses = true;
+		LLAresListener dummyLLAres("dummy_llares", respond_immediately, multiple_addresses);
+		dummyLLAres.listenTo(llaresPump);
+
+		LLXMLRPCListener dummyXMLRPC("dummy_xmlrpc");
+		dummyXMLRPC.listenTo(xmlrpcPump);
+
+		LLLogin login;
+
+		LoginListener listener("test_ear");
+		listener.listenTo(login.getEventPump());
+
+		LLSD credentials;
+		credentials["first"] = "foo";
+		credentials["last"] = "bar";
+		credentials["passwd"] = "secret";
+
+		login.connect("login.bar.com", credentials);
+
+		ensure_equals("SRV state", listener.lastEvent()["state"].asString(), "srvrequest"); 
+
+		dummyLLAres.sendReply();
+
+		// Test Authenticating State prior to first response.
+		ensure_equals("Auth state 1", listener.lastEvent()["state"].asString(), "authenticating"); 
+		ensure_equals("Attempt 1", listener.lastEvent()["data"]["attempt"].asInteger(), 1); 
+		ensure_equals("URI 1", listener.lastEvent()["data"]["request"]["uri"].asString(), "login.foo.com"); 
+
+		// First send emulated LLXMLRPCListener failure,
+		// this should return login to the authenticating step and increase the attempt 
+		// count.
+		LLSD data;
+		data["status"] = "OtherError"; 
+		data["errorcode"] = 0;
+		data["error"] = "dummy response";
+		data["transfer_rate"] = 0;
+		dummyXMLRPC.setResponse(data);
+		dummyXMLRPC.sendReply();
+
+		ensure_equals("Fail back to authenticate 1", listener.lastEvent()["state"].asString(), "authenticating"); 
+		ensure_equals("Attempt 2", listener.lastEvent()["data"]["attempt"].asInteger(), 2); 
+		ensure_equals("URI 2", listener.lastEvent()["data"]["request"]["uri"].asString(), "login.bar.com"); 
+
+		// Now send the 'indeterminate' response.
+		data.clear();
+		data["status"] = "Complete"; // StatusComplete
+		data["errorcode"] = 0;
+		data["error"] = "dummy response";
+		data["transfer_rate"] = 0;
+		data["responses"]["login"] = "indeterminate";
+		data["next_url"] = "login.indeterminate.com";			
+		data["next_method"] = "test_login_method"; 			
+		dummyXMLRPC.setResponse(data);
+		dummyXMLRPC.sendReply();
+
+		ensure_equals("Fail back to authenticate 2", listener.lastEvent()["state"].asString(), "authenticating"); 
+		ensure_equals("Attempt 3", listener.lastEvent()["data"]["attempt"].asInteger(), 3); 
+		ensure_equals("URI 3", listener.lastEvent()["data"]["request"]["uri"].asString(), "login.indeterminate.com"); 
+
+		// Finally let the auth succeed.
+		data.clear();
+		data["status"] = "Complete"; // StatusComplete
+		data["errorcode"] = 0;
+		data["error"] = "dummy response";
+		data["transfer_rate"] = 0;
+		data["responses"]["login"] = "true";
+		dummyXMLRPC.setResponse(data);
+		dummyXMLRPC.sendReply();
+
+		ensure_equals("Success state", listener.lastEvent()["state"].asString(), "online");
+
+		login.disconnect();
+
+		ensure_equals("Disconnected state", listener.lastEvent()["state"].asString(), "offline");
+	}
+
+    template<> template<>
+    void llviewerlogin_object::test<3>()
+    {
+		// Test completed response, that fails to login.
+		set_test_name("LLLogin valid response, failure (eg. bad credentials)");
+
+		// Testing normal login procedure.
+		LLEventStream llaresPump("LLAres"); // Dummy LLAres pump.
+		LLEventStream xmlrpcPump("LLXMLRPCTransaction"); // Dummy XMLRPC pump
+
+		LLAresListener dummyLLAres("dummy_llares");
+		dummyLLAres.listenTo(llaresPump);
+
+		LLXMLRPCListener dummyXMLRPC("dummy_xmlrpc");
+		dummyXMLRPC.listenTo(xmlrpcPump);
+
+		LLLogin login;
+		LoginListener listener("test_ear");
+		listener.listenTo(login.getEventPump());
+
+		LLSD credentials;
+		credentials["first"] = "who";
+		credentials["last"] = "what";
+		credentials["passwd"] = "badpasswd";
+
+		login.connect("login.bar.com", credentials);
+
+		ensure_equals("SRV state", listener.lastEvent()["state"].asString(), "srvrequest"); 
+
+		dummyLLAres.sendReply();
+
+		ensure_equals("Auth state", listener.lastEvent()["state"].asString(), "authenticating"); 
+
+		// Send the failed auth request reponse
+		LLSD data;
+		data["status"] = "Complete";
+		data["errorcode"] = 0;
+		data["error"] = "dummy response";
+		data["transfer_rate"] = 0;
+		data["responses"]["login"] = "false";
+		dummyXMLRPC.setResponse(data);
+		dummyXMLRPC.sendReply();
+
+		ensure_equals("Failed to offline", listener.lastEvent()["state"].asString(), "offline");
+	}
+
+    template<> template<>
+    void llviewerlogin_object::test<4>()
+    {
+		// Test incomplete response, that end the attempt.
+		set_test_name("LLLogin valid response, failure (eg. bad credentials)");
+
+		// Testing normal login procedure.
+		LLEventStream llaresPump("LLAres"); // Dummy LLAres pump.
+		LLEventStream xmlrpcPump("LLXMLRPCTransaction"); // Dummy XMLRPC pump
+
+		LLAresListener dummyLLAres("dummy_llares");
+		dummyLLAres.listenTo(llaresPump);
+
+		LLXMLRPCListener dummyXMLRPC("dummy_xmlrpc");
+		dummyXMLRPC.listenTo(xmlrpcPump);
+
+		LLLogin login;
+		LoginListener listener("test_ear");
+		listener.listenTo(login.getEventPump());
+
+		LLSD credentials;
+		credentials["first"] = "these";
+		credentials["last"] = "don't";
+		credentials["passwd"] = "matter";
+
+		login.connect("login.bar.com", credentials);
+
+		ensure_equals("SRV state", listener.lastEvent()["state"].asString(), "srvrequest"); 
+
+		dummyLLAres.sendReply();
+
+		ensure_equals("Auth state", listener.lastEvent()["state"].asString(), "authenticating"); 
+
+		// Send the failed auth request reponse
+		LLSD data;
+		data["status"] = "OtherError";
+		data["errorcode"] = 0;
+		data["error"] = "dummy response";
+		data["transfer_rate"] = 0;
+		dummyXMLRPC.setResponse(data);
+		dummyXMLRPC.sendReply();
+
+		ensure_equals("Failed to offline", listener.lastEvent()["state"].asString(), "offline");
+	}
+}
diff --git a/install.xml b/install.xml
index 24cbd3757555fa3a8c593b3c5e86086d3b1a3d10..0183193734406a866bcb2d6382899e427aa414e9 100644
--- a/install.xml
+++ b/install.xml
@@ -207,30 +207,30 @@
           <key>darwin</key>
           <map>
             <key>md5sum</key>
-            <string>081ef195a856c708cc473c4421b4b290</string>
+            <string>95dda5da1fb66b690a03944fca1b2c53</string>
             <key>url</key>
-            <uri>http://s3.amazonaws.com/viewer-source-downloads/install_pkgs/boost-1.34.1-darwin-20090223.tar.bz2</uri>
+            <uri>http://s3.amazonaws.com/viewer-source-downloads/install_pkgs/boost-1.34.1-darwin-20090427.tar.bz2</uri>
           </map>
           <key>linux</key>
           <map>
             <key>md5sum</key>
-            <string>b516a8576ecad0f957db7fc2cd972aac</string>
+            <string>33e2d48a6c2207ade0f914fff99c18af</string>
             <key>url</key>
-            <uri>http://s3.amazonaws.com/viewer-source-downloads/install_pkgs/boost-1.34.1-linux-20090223a.tar.bz2</uri>
+            <uri>http://s3.amazonaws.com/viewer-source-downloads/install_pkgs/boost-1.34.1-linux-20090427.tar.bz2</uri>
           </map>
           <key>linux64</key>
           <map>
             <key>md5sum</key>
-            <string>6db62bb7f141b3a1b3107e1f9aad0eb0</string>
+            <string>cadb1934581b20f9b03aa18e2be7c55c</string>
             <key>url</key>
-            <uri>http://s3.amazonaws.com/viewer-source-downloads/install_pkgs/boost-1.34.1-linux64-20090223a.tar.bz2</uri>
+            <uri>http://s3.amazonaws.com/viewer-source-downloads/install_pkgs/boost-1.34.1-linux64-20090427.tar.bz2</uri>
           </map>
           <key>windows</key>
           <map>
             <key>md5sum</key>
-            <string>3b56fe9e8d2975c612639d0a5370ffe7</string>
+            <string>c3ce8993eac0ca9546564d04131dc4f4</string>
             <key>url</key>
-            <uri>http://s3.amazonaws.com/viewer-source-downloads/install_pkgs/boost-1.34.1-windows-20090225.tar.bz2</uri>
+            <uri>http://s3.amazonaws.com/viewer-source-downloads/install_pkgs/boost-1.34.1-windows-20090430.tar.bz2</uri>
           </map>
         </map>
       </map>
@@ -1132,6 +1132,53 @@ anguage Infrstructure (CLI) international standard</string>
           </map>
         </map>
       </map>
+      <key>pth</key>
+      <map>
+        <key>copyright</key>
+        <string>Copyright (c) 1999-2006 Ralf S. Engelschall &lt;rse@gnu.org&gt;</string>
+        <key>description</key>
+        <string>Portable cooperative threads package, used to support Boost.Coroutine on Mac OS X 10.4</string>
+        <key>license</key>
+        <string>lgpl</string>
+        <key>packages</key>
+        <map>
+          <key>darwin</key>
+          <map>
+            <key>md5sum</key>
+            <string>533f4c710a209a6c4f205f81ccc0cfce</string>
+            <key>url</key>
+            <uri>http://s3.amazonaws.com/viewer-source-downloads/install_pkgs/pth-2.0.7-darwin-20090427.tar.bz2</uri>
+          </map>
+          <key>linux</key>
+          <map>
+            <key>md5sum</key>
+            <string>c5c2f73847c126e679d925beab48c7d4</string>
+            <key>url</key>
+            <uri>http://s3.amazonaws.com/viewer-source-downloads/install_pkgs/pth-2.0.7-linux-20090427.tar.bz2</uri>
+          </map>
+          <key>linux32</key>
+          <map>
+            <key>md5sum</key>
+            <string>c5c2f73847c126e679d925beab48c7d4</string>
+            <key>url</key>
+            <uri>http://s3.amazonaws.com/viewer-source-downloads/install_pkgs/pth-2.0.7-linux32-20090427.tar.bz2</uri>
+          </map>
+          <key>linux64</key>
+          <map>
+            <key>md5sum</key>
+            <string>c5c2f73847c126e679d925beab48c7d4</string>
+            <key>url</key>
+            <uri>http://s3.amazonaws.com/viewer-source-downloads/install_pkgs/pth-2.0.7-linux64-20090427.tar.bz2</uri>
+          </map>
+          <key>windows</key>
+          <map>
+            <key>md5sum</key>
+            <string>c5c2f73847c126e679d925beab48c7d4</string>
+            <key>url</key>
+            <uri>http://s3.amazonaws.com/viewer-source-downloads/install_pkgs/pth-2.0.7-windows-20090427.tar.bz2</uri>
+          </map>
+        </map>
+      </map>
       <key>quicktime</key>
       <map>
         <key>copyright</key>