summaryrefslogtreecommitdiff
path: root/indra
diff options
context:
space:
mode:
authorNat Goodspeed <nat@lindenlab.com>2023-01-06 19:58:12 -0500
committerNat Goodspeed <nat@lindenlab.com>2023-07-13 12:48:54 -0400
commitb2205bde52acf82575757f74a642c40b7433bf6b (patch)
treea360663c31d36f30575824a3c11c255ff8bb16eb /indra
parent8e61a8f7c0ebcc72a1c348e790eeb73b9388ccde (diff)
DRTVWR-558: Clean up LLEventDispatcher argument and result handling.
Add a new LLEventDispatcher constructor accepting not only the map key to extract a requested function name, but a second map key to extract the arguments -- when required. In Doxygen comments, clarify the difference between the two constructors. Move interaction with the LLEventPump subsystem to LLDispatchListener. LLEventDispatcher is intended to be directly called. On error, instead of looking for a "reply" key in the invocation LLSD, throw DispatchError. Publish DispatchError, formerly an implementation detail, and its new subclass DispatchMissing. Make both LLEventDispatcher::operator()() overloads return LLSD, leveraging the new internal ReturnLLSD logic that returns a degenerate LLSD blob for a void target callable and, for compatible types, converts the returned value to LLSD. Notably, the public try_call() overloads still return bool; any value returned by the target callable is discarded. Clarify the operator() and try_call() argument requirements for target callables registered to accept an LLSD array, in Doxygen comments and in code. In particular, the 'event' passed to (event) overloads (vs. the (name, event) overloads) must be an LLSD map, so it must contain an "args" key (or the new arguments map key specified to the constructor) containing the LLSD args array. Since the use of the new args key depends on whether the target callable is registered to accept an array or a map, pass it into DispatchEntry::call() (and all subclass overrides), along with a bool to disambiguate whether we reached that method from an LLEventDispatcher (event) invocation method or a (name, event) invocation method. Allow streaming an LLEventDispatcher instance to std::ostream, primarily to facilitate construction of proper error messages. Revert the 'name' argument of internal try_call(key, name, event) to std::string. Ditch try_call_log(), try_call_one() and reply(). Fold try_call_one() logic into three-argument try_call(). Refactor callFail() as a template method accepting both the exception to throw and arbitrary stringize() arguments from which to construct the exception message. Non-static callFail() implicitly prepends the instance and a colon to the rest of the arguments, and calls static sCallFail(). The latter constructs the exception message, logs it and throws the specified exception. This obviates try_call_log(). Make implementation detail helper class LLSDArgsMapper a private member of LLEventDispatcher so it can access sCallFail(): we now want all error handling to go through that method. Add LLSDArgsMapper::callFail() resembling LLSDEventDispatcher::callFail(), but without having to specify the exception: only LLEventDispatcher will throw anything but generic DispatchError. Give LLEventDispatcher::ParamsDispatchEntry and its subclasses ArrayParamsDispatchEntry and MapParamsDispatchEntry a new 'name' argument to identify error messages. Store it and use it implicitly in new callFail() method, very like LLSDArgsMapper::callFail(). Make LLEventDispatcher:: addArrayParamsDispatchEntry() and addMapParamsDispatchEntry() pass a 'name' that includes the LLEventDispatcher instance name as well as the name of the specific registered callable. This way we need not intercept a low-level error and annotate it with contextual data: we can just let the exception propagate. Make ParamsDispatchEntry::call() override catch LL::apply_error thrown by an invoker_function, and pass its message to callFail(), i.e. rethrow as LLEventDispatcher::DispatchError. Introduce ArrayParamsDispatchEntry::call() override for the special logic to extract an arguments array from a passed LLSD map -- but only under the circumstances described in the Doxygen comment. Add similar logic to MapParamsDispatchEntry::call(), but with both argskey itself and a value for argskey optional in the passed LLSD map. Because LLEventDispatcher now has two constructor overloads, allow subclass constructor LLDispatchListener() to accept zero or more trailing arguments. This is different than giving LLDispatchListener's constructor a default final argument, in that the subclass doesn't need to specify its default value: that's up to the base-class constructor. But it does require that the subclass constructor move to the header file. Move private LLEventDispatcher::reply() method to LLDispatchListener. Extend LLDispatchListener::process() to handle DispatchError by attempting to reply with a map containing an "error" key, per convention. (In other words, move that logic from LLEventDispatcher to LLDispatchListener.) Also, for a map LLSD result, attempt to reply with that result; for other defined LLSD types, attempt to reply with a map containing a "data" key. This is backwards compatible with previous behavior because all previous LLDispatchListener subclass methods returned void, which now produces an undefined LLSD blob, which we don't bother trying to send in reply. In lleventdispatcher_test.cpp, rework tut::lleventdispatcher_data::call_exc() yet again to catch DispatchError instead of listening for an LLEventPump reply event. Similarly, make call_logerr() catch DispatchError. Since the exception should also be logged, we ignore it and focus on the log, as before. Add tests <23> to <27>, exercising calls to new class DispatchResult methods returning string, int, LLSD map, LLSD array and void. (cherry picked from commit 2f9c915dd3d5137b5b2b1a57f0179e1f7a090f8c)
Diffstat (limited to 'indra')
-rw-r--r--indra/llcommon/lleventdispatcher.cpp428
-rw-r--r--indra/llcommon/lleventdispatcher.h166
-rw-r--r--indra/llcommon/tests/lleventdispatcher_test.cpp126
3 files changed, 463 insertions, 257 deletions
diff --git a/indra/llcommon/lleventdispatcher.cpp b/indra/llcommon/lleventdispatcher.cpp
index e7e73125a7..7e5723c503 100644
--- a/indra/llcommon/lleventdispatcher.cpp
+++ b/indra/llcommon/lleventdispatcher.cpp
@@ -43,21 +43,10 @@
#include "llexception.h"
#include "llsdutil.h"
#include "stringize.h"
+#include <iomanip> // std::quoted()
#include <memory> // std::auto_ptr
/*****************************************************************************
-* DispatchError
-*****************************************************************************/
-struct DispatchError: public LLException
-{
- // template constructor involving strings passes all arguments to
- // stringize() to construct LLException's what() string
- template <typename... ARGS>
- DispatchError(const std::string& arg0, ARGS&&... args):
- LLException(stringize(arg0, std::forward<ARGS>(args)...)) {}
-};
-
-/*****************************************************************************
* LLSDArgsMapper
*****************************************************************************/
/**
@@ -109,7 +98,7 @@ struct DispatchError: public LLException
* - Holes are filled with the default values.
* - Any remaining holes constitute an error.
*/
-class LL_COMMON_API LLSDArgsMapper
+class LL_COMMON_API LLEventDispatcher::LLSDArgsMapper
{
public:
/// Accept description of function: function name, param names, param
@@ -122,6 +111,8 @@ public:
private:
static std::string formatlist(const LLSD&);
+ template <typename... ARGS>
+ void callFail(ARGS&&... args) const;
// The function-name string is purely descriptive. We want error messages
// to be able to indicate which function's LLSDArgsMapper has the problem.
@@ -141,15 +132,16 @@ private:
FilledVector _has_dft;
};
-LLSDArgsMapper::LLSDArgsMapper(const std::string& function,
- const LLSD& names, const LLSD& defaults):
+LLEventDispatcher::LLSDArgsMapper::LLSDArgsMapper(const std::string& function,
+ const LLSD& names,
+ const LLSD& defaults):
_function(function),
_names(names),
_has_dft(names.size())
{
if (! (_names.isUndefined() || _names.isArray()))
{
- LLTHROW(DispatchError(function, " names must be an array, not ", names));
+ callFail(" names must be an array, not ", names);
}
auto nparams(_names.size());
// From _names generate _indexes.
@@ -172,8 +164,7 @@ LLSDArgsMapper::LLSDArgsMapper(const std::string& function,
// defaults is a (possibly empty) array. Right-align it with names.
if (ndefaults > nparams)
{
- LLTHROW(DispatchError(function, " names array ", names,
- " shorter than defaults array ", defaults));
+ callFail(" names array ", names, " shorter than defaults array ", defaults);
}
// Offset by which we slide defaults array right to right-align with
@@ -210,23 +201,20 @@ LLSDArgsMapper::LLSDArgsMapper(const std::string& function,
}
if (bogus.size())
{
- LLTHROW(DispatchError(function, " defaults specified for nonexistent params ",
- formatlist(bogus)));
+ callFail(" defaults specified for nonexistent params ", formatlist(bogus));
}
}
else
{
- LLTHROW(DispatchError(function, " defaults must be a map or an array, not ",
- defaults));
+ callFail(" defaults must be a map or an array, not ", defaults);
}
}
-LLSD LLSDArgsMapper::map(const LLSD& argsmap) const
+LLSD LLEventDispatcher::LLSDArgsMapper::map(const LLSD& argsmap) const
{
if (! (argsmap.isUndefined() || argsmap.isMap() || argsmap.isArray()))
{
- LLTHROW(DispatchError(_function, " map() needs a map or array, not ",
- argsmap));
+ callFail(" map() needs a map or array, not ", argsmap);
}
// Initialize the args array. Indexing a non-const LLSD array grows it
// to appropriate size, but we don't want to resize this one on each
@@ -323,15 +311,14 @@ LLSD LLSDArgsMapper::map(const LLSD& argsmap) const
// by argsmap, that's a problem.
if (unfilled.size())
{
- LLTHROW(DispatchError(_function, " missing required arguments ",
- formatlist(unfilled), " from ", argsmap));
+ callFail(" missing required arguments ", formatlist(unfilled), " from ", argsmap);
}
// done
return args;
}
-std::string LLSDArgsMapper::formatlist(const LLSD& list)
+std::string LLEventDispatcher::LLSDArgsMapper::formatlist(const LLSD& list)
{
std::ostringstream out;
const char* delim = "";
@@ -344,14 +331,26 @@ std::string LLSDArgsMapper::formatlist(const LLSD& list)
return out.str();
}
+template <typename... ARGS>
+void LLEventDispatcher::LLSDArgsMapper::callFail(ARGS&&... args) const
+{
+ LLEventDispatcher::sCallFail<LLEventDispatcher::DispatchError>
+ (_function, std::forward<ARGS>(args)...);
+}
+
/*****************************************************************************
* LLEventDispatcher
*****************************************************************************/
LLEventDispatcher::LLEventDispatcher(const std::string& desc, const std::string& key):
+ LLEventDispatcher(desc, key, "args")
+{}
+
+LLEventDispatcher::LLEventDispatcher(const std::string& desc, const std::string& key,
+ const std::string& argskey):
mDesc(desc),
- mKey(key)
-{
-}
+ mKey(key),
+ mArgskey(argskey)
+{}
LLEventDispatcher::~LLEventDispatcher()
{
@@ -375,20 +374,21 @@ struct LLEventDispatcher::LLSDDispatchEntry: public LLEventDispatcher::DispatchE
Callable mFunc;
LLSD mRequired;
- virtual LLSD call(const std::string& desc, const LLSD& event) const
+ LLSD call(const std::string& desc, const LLSD& event, bool, const std::string&) const override
{
// Validate the syntax of the event itself.
std::string mismatch(llsd_matches(mRequired, event));
if (! mismatch.empty())
{
- LLTHROW(DispatchError(desc, ": bad request: ", mismatch));
+ LLEventDispatcher::sCallFail<LLEventDispatcher::DispatchError>
+ (desc, ": bad request: ", mismatch);
}
// Event syntax looks good, go for it!
mFunc(event);
return {};
}
- virtual LLSD addMetadata(LLSD meta) const
+ LLSD addMetadata(LLSD meta) const override
{
meta["required"] = mRequired;
return meta;
@@ -401,16 +401,35 @@ struct LLEventDispatcher::LLSDDispatchEntry: public LLEventDispatcher::DispatchE
*/
struct LLEventDispatcher::ParamsDispatchEntry: public LLEventDispatcher::DispatchEntry
{
- ParamsDispatchEntry(const std::string& desc, const invoker_function& func):
+ ParamsDispatchEntry(const std::string& name, const std::string& desc,
+ const invoker_function& func):
DispatchEntry(desc),
+ mName(name),
mInvoker(func)
{}
+ std::string mName;
invoker_function mInvoker;
- virtual LLSD call(const std::string&, const LLSD& event) const
+ LLSD call(const std::string&, const LLSD& event, bool, const std::string&) const override
+ {
+ try
+ {
+ return mInvoker(event);
+ }
+ catch (const LL::apply_error& err)
+ {
+ // could hit runtime errors with LL::apply()
+ return callFail(err.what());
+ }
+ }
+
+ template <typename... ARGS>
+ LLSD callFail(ARGS&&... args) const
{
- return mInvoker(event);
+ LLEventDispatcher::sCallFail<LLEventDispatcher::DispatchError>(mName, ": ", std::forward<ARGS>(args)...);
+ // pacify the compiler
+ return {};
}
};
@@ -420,15 +439,48 @@ struct LLEventDispatcher::ParamsDispatchEntry: public LLEventDispatcher::Dispatc
*/
struct LLEventDispatcher::ArrayParamsDispatchEntry: public LLEventDispatcher::ParamsDispatchEntry
{
- ArrayParamsDispatchEntry(const std::string& desc, const invoker_function& func,
- LLSD::Integer arity):
- ParamsDispatchEntry(desc, func),
+ ArrayParamsDispatchEntry(const std::string& name, const std::string& desc,
+ const invoker_function& func, LLSD::Integer arity):
+ ParamsDispatchEntry(name, desc, func),
mArity(arity)
{}
LLSD::Integer mArity;
- virtual LLSD addMetadata(LLSD meta) const
+ LLSD call(const std::string& desc, const LLSD& event, bool fromMap, const std::string& argskey) const override
+ {
+// std::string context { stringize(desc, "(", event, ") with argskey ", std::quoted(argskey), ": ") };
+ // Whether we try to extract arguments from 'event' depends on whether
+ // the LLEventDispatcher consumer called one of the (name, event)
+ // methods (! fromMap) or one of the (event) methods (fromMap). If we
+ // were called with (name, event), the passed event must itself be
+ // suitable to pass to the registered callable, no args extraction
+ // required or even attempted. Only if called with plain (event) do we
+ // consider extracting args from that event. Initially assume 'event'
+ // itself contains the arguments.
+ LLSD args{ event };
+ if (fromMap)
+ {
+ if (mArity)
+ {
+ // We only require/retrieve argskey if the target function
+ // isn't nullary. For all others, since we require an LLSD
+ // array, we must have an argskey.
+ if (argskey.empty())
+ {
+ return callFail("LLEventDispatcher has no args key");
+ }
+ if ((! event.has(argskey)))
+ {
+ return callFail("missing required key ", std::quoted(argskey));
+ }
+ args = event[argskey];
+ }
+ }
+ return ParamsDispatchEntry::call(desc, args, fromMap, argskey);
+ }
+
+ LLSD addMetadata(LLSD meta) const override
{
LLSD array(LLSD::emptyArray());
// Resize to number of arguments required
@@ -449,7 +501,7 @@ struct LLEventDispatcher::MapParamsDispatchEntry: public LLEventDispatcher::Para
MapParamsDispatchEntry(const std::string& name, const std::string& desc,
const invoker_function& func,
const LLSD& params, const LLSD& defaults):
- ParamsDispatchEntry(desc, func),
+ ParamsDispatchEntry(name, desc, func),
mMapper(name, params, defaults),
mRequired(LLSD::emptyMap())
{
@@ -493,14 +545,25 @@ struct LLEventDispatcher::MapParamsDispatchEntry: public LLEventDispatcher::Para
LLSD mRequired;
LLSD mOptional;
- virtual LLSD call(const std::string& desc, const LLSD& event) const
+ LLSD call(const std::string& desc, const LLSD& event, bool fromMap, const std::string& argskey) const override
{
- // Just convert from LLSD::Map to LLSD::Array using mMapper, then pass
- // to base-class call() method.
- return ParamsDispatchEntry::call(desc, mMapper.map(event));
+ // by default, pass the whole event as the arguments map
+ LLSD args{ event };
+ // Were we called by one of the (event) methods (instead of the (name,
+ // event) methods), do we have an argskey, and does the incoming event
+ // have that key?
+ if (fromMap && (! argskey.empty()) && event.has(argskey))
+ {
+ // if so, extract the value of argskey from the incoming event,
+ // and use that as the arguments map
+ args = event[argskey];
+ }
+ // Now convert args from LLSD map to LLSD array using mMapper, then
+ // pass to base-class call() method.
+ return ParamsDispatchEntry::call(desc, mMapper.map(args), fromMap, argskey);
}
- virtual LLSD addMetadata(LLSD meta) const
+ LLSD addMetadata(LLSD meta) const override
{
meta["required"] = mRequired;
meta["optional"] = mOptional;
@@ -513,7 +576,11 @@ void LLEventDispatcher::addArrayParamsDispatchEntry(const std::string& name,
const invoker_function& invoker,
LLSD::Integer arity)
{
- mDispatch.emplace(name, new ArrayParamsDispatchEntry(desc, invoker, arity));
+ // The first parameter to ArrayParamsDispatchEntry is solely for error
+ // messages. Identify our instance and this entry.
+ mDispatch.emplace(
+ name,
+ new ArrayParamsDispatchEntry(stringize(*this, '[', name, ']'), desc, invoker, arity));
}
void LLEventDispatcher::addMapParamsDispatchEntry(const std::string& name,
@@ -522,7 +589,11 @@ void LLEventDispatcher::addMapParamsDispatchEntry(const std::string& name,
const LLSD& params,
const LLSD& defaults)
{
- mDispatch.emplace(name, new MapParamsDispatchEntry(name, desc, invoker, params, defaults));
+ // Pass instance info as well as this entry name for error messages.
+ mDispatch.emplace(
+ name,
+ new MapParamsDispatchEntry(stringize(*this, '[', name, ']'),
+ desc, invoker, params, defaults));
}
/// Register a callable by name
@@ -546,174 +617,101 @@ bool LLEventDispatcher::remove(const std::string& name)
/// Call a registered callable with an explicitly-specified name. It is an
/// error if no such callable exists.
-void LLEventDispatcher::operator()(const std::string& name, const LLSD& event) const
+LLSD LLEventDispatcher::operator()(const std::string& name, const LLSD& event) const
{
- std::string error{ try_call_log(std::string(), name, event) };
- if (! error.empty())
- {
- callFail(event, error);
- }
+ return try_call(std::string(), name, event);
}
-/// Extract the @a key value from the incoming @a event, and call the callable
-/// whose name is specified by that map @a key. It is an error if no such
-/// callable exists.
-void LLEventDispatcher::operator()(const LLSD& event) const
+bool LLEventDispatcher::try_call(const std::string& name, const LLSD& event) const
{
- std::string error{ try_call_log(mKey, event[mKey], event) };
- if (! error.empty())
+ try
{
- callFail(event, error);
+ try_call(std::string(), name, event);
+ return true;
}
-}
-
-void LLEventDispatcher::callFail(const LLSD& event, const std::string& msg) const
-{
- // pass back a response that includes an "error" key with the message.
- reply(llsd::map("error", msg), event);
-}
-
-void LLEventDispatcher::reply(const LLSD& response, const LLSD& event) const
-{
- static LLSD::String key{ "reply" };
- if (event.has(key))
+ // Note that we don't catch the generic DispatchError, only the specific
+ // DispatchMissing. try_call() only promises to return false if the
+ // specified callable name isn't found -- not for general errors.
+ catch (const DispatchMissing&)
{
- // Oh good, the incoming event specifies a reply pump -- pass back
- // our response.
- sendReply(response, event, key);
+ return false;
}
}
-bool LLEventDispatcher::try_call(const LLSD& event) const
-{
- return try_call_log(mKey, event[mKey], event).empty();
-}
-
-/*==========================================================================*|
- TODO:
-
-* When mInvoker returns result.isDefined(), sendReply(llsd::map("data", result))
-* When try_call finds name.isArray(), construct response array from
- dispatching each call, sendReply() as above
-* When try_call finds name.isMap(), construct response map from dispatching
- each call, sendReply() as above -- note, caller can't care about order
-* Possible future transactional behavior: look up all names before calling any
-
-|*==========================================================================*/
-bool LLEventDispatcher::try_call(const std::string& name, const LLSD& event) const
+/// Extract the @a key value from the incoming @a event, and call the callable
+/// whose name is specified by that map @a key. It is an error if no such
+/// callable exists.
+LLSD LLEventDispatcher::operator()(const LLSD& event) const
{
- return try_call_log(std::string(), name, event).empty();
+ return try_call(mKey, event[mKey], event);
}
-std::string LLEventDispatcher::try_call_log(const std::string& key, const LLSD& name,
- const LLSD& event) const
+bool LLEventDispatcher::try_call(const LLSD& event) const
{
- std::string error{ try_call(key, name, event) };
- if (! error.empty())
+ try
{
- // If we're a subclass of LLEventDispatcher, e.g. LLEventAPI, report that.
- error = stringize(LLError::Log::classname(this), "(", mDesc, "): ", error);
- LL_WARNS("LLEventDispatcher") << error << LL_ENDL;
+ try_call(mKey, event[mKey], event);
+ return true;
+ }
+ catch (const DispatchMissing&)
+ {
+ return false;
}
- return error;
}
-// This internal method returns empty string if the call succeeded, else
-// non-empty error message.
-std::string LLEventDispatcher::try_call(const std::string& key, const LLSD& name,
- const LLSD& event) const
+LLSD LLEventDispatcher::try_call(const std::string& key, const std::string& name,
+ const LLSD& event) const
{
- if (name.isUndefined())
+ if (name.empty())
{
if (key.empty())
{
- return "attempting to call with no name";
+ callFail<DispatchError>("attempting to call with no name");
}
else
{
- return stringize("no ", key);
- }
- }
- else if (name.isArray())
- {
- return stringize(key, " array dispatch ", name, " not yet implemented");
- }
- else if (name.isMap())
- {
- return stringize(key, " map dispatch ", name, " not yet implemented");
- }
- else if (! name.isString())
- {
- return stringize(key, " bad type ", LLSD::typeString(name.type()), ' ', name,
- " -- function names are String");
- }
- else // name is an LLSD::String
- {
- auto success{ try_call_one(key, name, event) };
- // pretend to unpack
- std::string& error{ success.first };
- LLSD& result{ success.second };
- // did try_call_one() report an error?
- if (! error.empty())
- {
- // if we have a reply key, respond to invoker
- reply(llsd::map("error", error), event);
- // now tell caller
- return error;
- }
- // try_call_one() succeeded in calling the target function --
- // should we reply to invoker?
- if (result.isUndefined())
- {
- // We would get result.isUndefined() if the target function has
- // void return. In any case, even if the target function returns
- // LLSD, isUndefined() means "don't bother sending response."
- return {};
- }
- // result.isDefined(): the target function returned something.
- // Respond to invoker if we have a "reply" key.
- if (! result.isMap())
- {
- // wrap result in a map to play well with sendReply()
- result = llsd::map("data", result);
+ callFail<DispatchError>("no ", key);
}
- reply(result, event);
- return {};
}
-}
-std::pair<std::string, LLSD>
-LLEventDispatcher::try_call_one(const std::string& key, const std::string& name,
- const LLSD& event) const
-{
DispatchMap::const_iterator found = mDispatch.find(name);
if (found == mDispatch.end())
{
+ // Here we were passed a valid name, but there's no registered
+ // callable with that name. This is the one case in which we throw
+ // DispatchMissing instead of the generic DispatchError.
+ // Distinguish the public method by which our caller reached here:
+ // key.empty() means the name was passed explicitly, non-empty means
+ // we extracted the name from the incoming event using that key.
if (key.empty())
{
- return { stringize("'", name, "' not found"), {} };
+ callFail<DispatchMissing>(std::quoted(name), " not found");
}
else
{
- return { stringize("bad ", key, " value '", name, "'"), {} };
+ callFail<DispatchMissing>("bad ", key, " value ", std::quoted(name));
}
}
- try
- {
- // Found the name, so it's plausible to even attempt the call.
- return { {}, found->second->call(stringize("calling '", name, "'"), event) };
- }
- catch (const DispatchError& err)
- {
- // trouble preparing arguments
- return { err.what(), {} };
- }
- catch (const LL::apply_error& err)
- {
- // could also hit runtime errors with LL::apply()
- return { err.what(), {} };
- }
+ // Found the name, so it's plausible to even attempt the call.
+ return found->second->call(stringize(*this, " calling ", std::quoted(name)),
+ event, (! key.empty()), mArgskey);
+}
+
+template <typename EXCEPTION, typename... ARGS>
+//static
+void LLEventDispatcher::sCallFail(ARGS&&... args)
+{
+ auto error = stringize(std::forward<ARGS>(args)...);
+ LL_WARNS("LLEventDispatcher") << error << LL_ENDL;
+ LLTHROW(EXCEPTION(error));
+}
+
+template <typename EXCEPTION, typename... ARGS>
+void LLEventDispatcher::callFail(ARGS&&... args) const
+{
+ // Describe this instance in addition to the error itself.
+ sCallFail<EXCEPTION>(*this, ": ", std::forward<ARGS>(args)...);
}
LLSD LLEventDispatcher::getMetadata(const std::string& name) const
@@ -729,27 +727,69 @@ LLSD LLEventDispatcher::getMetadata(const std::string& name) const
return found->second->addMetadata(meta);
}
+std::ostream& operator<<(std::ostream& out, const LLEventDispatcher& self)
+{
+ // If we're a subclass of LLEventDispatcher, e.g. LLEventAPI, report that.
+ return out << LLError::Log::classname(self) << '(' << self.mDesc << ')';
+}
+
/*****************************************************************************
* LLDispatchListener
*****************************************************************************/
-LLDispatchListener::LLDispatchListener(const std::string& pumpname, const std::string& key):
- LLEventDispatcher(pumpname, key),
- // Do NOT tweak the passed pumpname. In practice, when someone
- // instantiates a subclass of our LLEventAPI subclass, they intend to
- // claim that LLEventPump name in the global LLEventPumps namespace. It
- // would be mysterious and distressing if we allowed name tweaking, and
- // someone else claimed pumpname first for a completely unrelated
- // LLEventPump. Posted events would never reach our subclass listener
- // because we would have silently changed its name; meanwhile listeners
- // (if any) on that other LLEventPump would be confused by the events
- // intended for our subclass.
- LLEventStream(pumpname, false),
- mBoundListener(listen("self", [this](const LLSD& event){ return process(event); }))
+/*==========================================================================*|
+ TODO:
+* When process() finds name.isArray(), construct response array from
+ dispatching each call -- args must also be (array of args structures)
+ (could also construct response map, IF array contains unique names)
+* When process() finds name.isMap(), construct response map from dispatching
+ each call -- value of each key is its args struct -- argskey ignored --
+ note, caller can't care about order
+* Possible future transactional behavior: look up all names before calling any
+|*==========================================================================*/
+std::string LLDispatchListener::mReplyKey{ "reply" };
+
+bool LLDispatchListener::process(const LLSD& event) const
{
+ LLSD result;
+ try
+ {
+ result = (*this)(event);
+ }
+ catch (const DispatchError& err)
+ {
+ // If the incoming event contains a "reply" key, we'll respond to the
+ // invoker with an error message (below). But if not -- silently
+ // ignoring an invocation request would be confusing at best. Escalate.
+ if (! event.has(mReplyKey))
+ {
+ throw;
+ }
+
+ // reply with a map containing an "error" key explaining the problem
+ reply(llsd::map("error", err.what()), event);
+ return false;
+ }
+
+ // We seem to have gotten a valid result. But we don't know whether the
+ // registered callable is void or non-void. If it's void,
+ // LLEventDispatcher will return isUndefined(). Otherwise, try to send it
+ // back to our invoker.
+ if (result.isDefined())
+ {
+ if (! result.isMap())
+ {
+ // wrap the result in a map as the "data" key
+ result = llsd::map("data", result);
+ }
+ reply(result, event);
+ }
+ return false;
}
-bool LLDispatchListener::process(const LLSD& event)
+void LLDispatchListener::reply(const LLSD& reply, const LLSD& request) const
{
- (*this)(event);
- return false;
+ // Call sendReply() unconditionally: sendReply() itself tests whether the
+ // specified reply key is present in the incoming request, and does
+ // nothing if there's no such key.
+ sendReply(reply, request, mReplyKey);
}
diff --git a/indra/llcommon/lleventdispatcher.h b/indra/llcommon/lleventdispatcher.h
index cebce618df..494fc6a366 100644
--- a/indra/llcommon/lleventdispatcher.h
+++ b/indra/llcommon/lleventdispatcher.h
@@ -57,7 +57,20 @@ class LLSD;
class LL_COMMON_API LLEventDispatcher
{
public:
+ /**
+ * Pass description and the LLSD key used by try_call(const LLSD&) and
+ * operator()(const LLSD&) to extract the name of the registered callable
+ * to invoke.
+ */
LLEventDispatcher(const std::string& desc, const std::string& key);
+ /**
+ * Pass description, the LLSD key used by try_call(const LLSD&) and
+ * operator()(const LLSD&) to extract the name of the registered callable
+ * to invoke, and the LLSD key used by try_call(const LLSD&) and
+ * operator()(const LLSD&) to extract arguments LLSD.
+ */
+ LLEventDispatcher(const std::string& desc, const std::string& key,
+ const std::string& argskey);
virtual ~LLEventDispatcher();
/// @name Register functions accepting(const LLSD&)
@@ -146,6 +159,8 @@ public:
* boost::lambda::var(instance) or boost::lambda::constant(instance_ptr)
* produce suitable callables.
*
+ * TODO: variant accepting a method of the containing class, no getter.
+ *
* When calling this name, pass an LLSD::Array. Each entry in turn will be
* converted to the corresponding parameter type using LLSDParam.
*/
@@ -185,6 +200,8 @@ public:
* boost::lambda::var(instance) or boost::lambda::constant(instance_ptr)
* produce suitable callables.
*
+ * TODO: variant accepting a method of the containing class, no getter.
+ *
* Pass an LLSD::Array of parameter names, and optionally another
* LLSD::Array of default parameter values, a la LLSDArgsMapper.
*
@@ -206,28 +223,82 @@ public:
/// Unregister a callable
bool remove(const std::string& name);
- /// Call a registered callable with an explicitly-specified name. It is an
- /// error if no such callable exists. It is an error if the @a event fails
- /// to match the @a required prototype specified at add() time.
- void operator()(const std::string& name, const LLSD& event) const;
+ /// Exception if an attempted call fails for any reason
+ struct DispatchError: public LLException
+ {
+ DispatchError(const std::string& what): LLException(what) {}
+ };
+
+ /// Specific exception for an attempt to call a nonexistent name
+ struct DispatchMissing: public DispatchError
+ {
+ DispatchMissing(const std::string& what): DispatchError(what) {}
+ };
+
+ /**
+ * Call a registered callable with an explicitly-specified name,
+ * converting its return value to LLSD (undefined for a void callable).
+ * It is an error if no such callable exists. It is an error if the @a
+ * event fails to match the @a required prototype specified at add()
+ * time.
+ *
+ * @a event must be an LLSD array for a callable registered to accept its
+ * arguments from such an array. It must be an LLSD map for a callable
+ * registered to accept its arguments from such a map.
+ */
+ LLSD operator()(const std::string& name, const LLSD& event) const;
- /// Call a registered callable with an explicitly-specified name and
- /// return <tt>true</tt>. If no such callable exists, return
- /// <tt>false</tt>. It is an error if the @a event fails to match the @a
- /// required prototype specified at add() time.
+ /**
+ * Call a registered callable with an explicitly-specified name and
+ * return <tt>true</tt>. If no such callable exists, return
+ * <tt>false</tt>. It is an error if the @a event fails to match the @a
+ * required prototype specified at add() time.
+ *
+ * @a event must be an LLSD array for a callable registered to accept its
+ * arguments from such an array. It must be an LLSD map for a callable
+ * registered to accept its arguments from such a map.
+ */
bool try_call(const std::string& name, const LLSD& event) const;
- /// Extract the @a key value from the incoming @a event, and call the
- /// callable whose name is specified by that map @a key. It is an error if
- /// no such callable exists. It is an error if the @a event fails to match
- /// the @a required prototype specified at add() time.
- void operator()(const LLSD& event) const;
-
- /// Extract the @a key value from the incoming @a event, call the callable
- /// whose name is specified by that map @a key and return <tt>true</tt>.
- /// If no such callable exists, return <tt>false</tt>. It is an error if
- /// the @a event fails to match the @a required prototype specified at
- /// add() time.
+ /**
+ * Extract the @a key specified to our constructor from the incoming LLSD
+ * map @a event, and call the callable whose name is specified by that @a
+ * key's value, converting its return value to LLSD (undefined for a void
+ * callable). It is an error if no such callable exists. It is an error if
+ * the @a event fails to match the @a required prototype specified at
+ * add() time.
+ *
+ * For a (non-nullary) callable registered to accept its arguments from an
+ * LLSD array, the @a event map must contain the key @a argskey specified to
+ * our constructor. The value of the @a argskey key must be an LLSD array
+ * containing the arguments to pass to the callable named by @a key.
+ *
+ * For a callable registered to accept its arguments from an LLSD map, if
+ * the @a event map contains the key @a argskey specified our constructor,
+ * extract the value of the @a argskey key and use it as the arguments map.
+ * If @a event contains no @a argskey key, use the whole @a event as the
+ * arguments map.
+ */
+ LLSD operator()(const LLSD& event) const;
+
+ /**
+ * Extract the @a key specified to our constructor from the incoming LLSD
+ * map @a event, call the callable whose name is specified by that @a
+ * key's value and return <tt>true</tt>. If no such callable exists,
+ * return <tt>false</tt>. It is an error if the @a event fails to match
+ * the @a required prototype specified at add() time.
+ *
+ * For a (non-nullary) callable registered to accept its arguments from an
+ * LLSD array, the @a event map must contain the key @a argskey specified to
+ * our constructor. The value of the @a argskey key must be an LLSD array
+ * containing the arguments to pass to the callable named by @a key.
+ *
+ * For a callable registered to accept its arguments from an LLSD map, if
+ * the @a event map contains the key @a argskey specified our constructor,
+ * extract the value of the @a argskey key and use it as the arguments map.
+ * If @a event contains no @a argskey key, use the whole @a event as the
+ * arguments map.
+ */
bool try_call(const LLSD& event) const;
/// @name Iterate over defined names
@@ -242,7 +313,8 @@ private:
std::string mDesc;
- virtual LLSD call(const std::string& desc, const LLSD& event) const = 0;
+ virtual LLSD call(const std::string& desc, const LLSD& event,
+ bool fromMap, const std::string& argskey) const = 0;
virtual LLSD addMetadata(LLSD) const = 0;
};
typedef std::map<std::string, std::unique_ptr<DispatchEntry> > DispatchMap;
@@ -269,6 +341,9 @@ public:
/// Retrieve the LLSD key we use for one-arg <tt>operator()</tt> method
std::string getDispatchKey() const { return mKey; }
+ /// description of this instance's leaf class and description
+ friend std::ostream& operator<<(std::ostream&, const LLEventDispatcher&);
+
private:
template <class CLASS, typename METHOD>
void addMethod(const std::string& name, const std::string& desc,
@@ -285,19 +360,16 @@ private:
}
}
void addFail(const std::string& name, const std::string& classname) const;
- std::string try_call_log(const std::string& key, const LLSD& name,
- const LLSD& event) const;
- std::string try_call(const std::string& key, const LLSD& name,
- const LLSD& event) const;
- // returns either (empty string, LLSD) or (error message, isUndefined)
- std::pair<std::string, LLSD>
- try_call_one(const std::string& key, const std::string& name, const LLSD& event) const;
- // Implement "it is an error" semantics for attempted call operations: if
- // the incoming event includes a "reply" key, log and send an error reply.
- void callFail(const LLSD& event, const std::string& msg) const;
- void reply(const LLSD& response, const LLSD& event) const;
-
- std::string mDesc, mKey;
+ LLSD try_call(const std::string& key, const std::string& name,
+ const LLSD& event) const;
+ // raise specified EXCEPTION with specified stringize(ARGS)
+ template <typename EXCEPTION, typename... ARGS>
+ void callFail(ARGS&&... args) const;
+ template <typename EXCEPTION, typename... ARGS>
+ static
+ void sCallFail(ARGS&&... args);
+
+ std::string mDesc, mKey, mArgskey;
DispatchMap mDispatch;
static NameDesc makeNameDesc(const DispatchMap::value_type& item)
@@ -305,6 +377,7 @@ private:
return NameDesc(item.first, item.second->mDesc);
}
+ class LLSDArgsMapper;
struct LLSDDispatchEntry;
struct ParamsDispatchEntry;
struct ArrayParamsDispatchEntry;
@@ -459,13 +532,36 @@ class LL_COMMON_API LLDispatchListener:
public LLEventStream
{
public:
- LLDispatchListener(const std::string& pumpname, const std::string& key);
+ template <typename... ARGS>
+ LLDispatchListener(const std::string& pumpname, const std::string& key,
+ ARGS&&... args);
virtual ~LLDispatchListener() {}
private:
- bool process(const LLSD& event);
+ bool process(const LLSD& event) const;
+ void reply(const LLSD& reply, const LLSD& request) const;
LLTempBoundListener mBoundListener;
+ static std::string mReplyKey;
};
+template <typename... ARGS>
+LLDispatchListener::LLDispatchListener(const std::string& pumpname, const std::string& key,
+ ARGS&&... args):
+ // pass through any additional arguments to LLEventDispatcher ctor
+ LLEventDispatcher(pumpname, key, std::forward<ARGS>(args)...),
+ // Do NOT tweak the passed pumpname. In practice, when someone
+ // instantiates a subclass of our LLEventAPI subclass, they intend to
+ // claim that LLEventPump name in the global LLEventPumps namespace. It
+ // would be mysterious and distressing if we allowed name tweaking, and
+ // someone else claimed pumpname first for a completely unrelated
+ // LLEventPump. Posted events would never reach our subclass listener
+ // because we would have silently changed its name; meanwhile listeners
+ // (if any) on that other LLEventPump would be confused by the events
+ // intended for our subclass.
+ LLEventStream(pumpname, false),
+ mBoundListener(listen("self", [this](const LLSD& event){ return process(event); }))
+{
+}
+
#endif /* ! defined(LL_LLEVENTDISPATCHER_H) */
diff --git a/indra/llcommon/tests/lleventdispatcher_test.cpp b/indra/llcommon/tests/lleventdispatcher_test.cpp
index f09dd63316..00bdff89e5 100644
--- a/indra/llcommon/tests/lleventdispatcher_test.cpp
+++ b/indra/llcommon/tests/lleventdispatcher_test.cpp
@@ -18,6 +18,7 @@
// external library headers
// other Linden headers
#include "../test/lltut.h"
+#include "lleventfilter.h"
#include "llsd.h"
#include "llsdutil.h"
#include "llevents.h"
@@ -640,42 +641,38 @@ namespace tut
std::string call_exc(const std::string& func, const LLSD& args, const std::string& exc_frag)
{
- // This method was written when LLEventDispatcher responded to
- // name or argument errors with LL_ERRS, hence the name: we used
- // to have to intercept LL_ERRS by making it throw. Now we set up
- // to catch an error response instead. But -- for that we need to
- // be able to sneak a "reply" key into args, which must be a Map.
- if (! (args.isUndefined() or args.isMap()))
- fail(stringize("can't test call_exc() with ", args));
- LLEventStream replypump("reply");
- LLSD reply;
- LLTempBoundListener bound{
- replypump.listen(
- "listener",
- [&reply](const LLSD& event)
- {
- reply = event;
- return false;
- }) };
- LLSD modargs{ args };
- modargs["reply"] = replypump.getName();
- if (func.empty())
+ std::string what;
+ try
{
- work(modargs);
+ if (func.empty())
+ {
+ work(args);
+ }
+ else
+ {
+ work(func, args);
+ }
}
- else
+ catch (const LLEventDispatcher::DispatchError& err)
{
- work(func, modargs);
+ what = err.what();
}
- ensure("no error response", reply.has("error"));
- ensure_has(reply["error"], exc_frag);
- return reply["error"];
+ ensure_has(what, exc_frag);
+ return what;
}
void call_logerr(const std::string& func, const LLSD& args, const std::string& frag)
{
CaptureLog capture;
- work(func, args);
+ try
+ {
+ work(func, args);
+ }
+ catch (const LLEventDispatcher::DispatchError& err)
+ {
+ // the error should also have been logged; we just need to
+ // stop the exception propagating
+ }
capture.messageWith(frag);
}
@@ -1017,6 +1014,10 @@ namespace tut
(llsd::array("freenb_map_mdft", "smethodnb_map_mdft", "methodnb_map_mdft"),
llsd::array(LLSD::emptyMap(), dft_map_full["b"])))); // required, optional
+ llsd::array // group
+ (llsd::array("freenb_map_mdft", "smethodnb_map_mdft", "methodnb_map_mdft"),
+ llsd::array(LLSD::emptyMap(), dft_map_full["b"])))); // required, optional
+
for (LLSD grp: inArray(groups))
{
// Internal structure of each group in 'groups':
@@ -1196,7 +1197,7 @@ namespace tut
template<> template<>
void object::test<20>()
{
- set_test_name("call array-style functions with (just right | too long) arrays");
+ set_test_name("call array-style functions with right-size arrays");
std::vector<U8> binary;
for (size_t h(0x01), i(0); i < 5; h+= 0x22, ++i)
{
@@ -1326,4 +1327,73 @@ namespace tut
}
}
}
+
+ struct DispatchResult: public LLEventDispatcher
+ {
+ using DR = DispatchResult;
+
+ DispatchResult(): LLEventDispatcher("expect result", "op")
+ {
+ // As of 2022-12-22, LLEventDispatcher's shorthand add() methods
+ // for pointer-to-method of same instance only support methods
+ // with signature void(const LLSD&). The generic add(pointer-to-
+ // method) requires an instance getter.
+ add("strfunc", "return string", &DR::strfunc, [this](){ return this; });
+ add("voidfunc", "void function", &DR::voidfunc, [this](){ return this; });
+ add("intfunc", "return Integer LLSD", &DR::intfunc, [this](){ return this; });
+ add("mapfunc", "return map LLSD", &DR::mapfunc, [this](){ return this; });
+ add("arrayfunc", "return array LLSD", &DR::arrayfunc, [this](){ return this; });
+ }
+
+ std::string strfunc(const LLSD&) const { return "a string"; }
+ void voidfunc() const {}
+ int intfunc(const LLSD&) const { return 17; }
+ LLSD mapfunc(const LLSD&) const { return llsd::map("key", "value"); }
+ LLSD arrayfunc(const LLSD&) const { return llsd::array("a", "b", "c"); }
+ };
+
+ template<> template<>
+ void object::test<23>()
+ {
+ set_test_name("string result");
+ DispatchResult service;
+ LLSD result{ service("strfunc", "ignored") };
+ ensure_equals("strfunc() mismatch", result.asString(), "a string");
+ }
+
+ template<> template<>
+ void object::test<24>()
+ {
+ set_test_name("void result");
+ DispatchResult service;
+ LLSD result{ service("voidfunc", LLSD()) };
+ ensure("voidfunc() returned defined", result.isUndefined());
+ }
+
+ template<> template<>
+ void object::test<25>()
+ {
+ set_test_name("Integer result");
+ DispatchResult service;
+ LLSD result{ service("intfunc", "ignored") };
+ ensure_equals("intfunc() mismatch", result.asInteger(), 17);
+ }
+
+ template<> template<>
+ void object::test<26>()
+ {
+ set_test_name("map LLSD result");
+ DispatchResult service;
+ LLSD result{ service("mapfunc", "ignored") };
+ ensure_equals("mapfunc() mismatch", result, llsd::map("key", "value"));
+ }
+
+ template<> template<>
+ void object::test<27>()
+ {
+ set_test_name("array LLSD result");
+ DispatchResult service;
+ LLSD result{ service("arrayfunc", "ignored") };
+ ensure_equals("arrayfunc() mismatch", result, llsd::array("a", "b", "c"));
+ }
} // namespace tut