summaryrefslogtreecommitdiff
path: root/indra/llcommon
diff options
context:
space:
mode:
Diffstat (limited to 'indra/llcommon')
-rw-r--r--indra/llcommon/CMakeLists.txt38
-rw-r--r--indra/llcommon/coro_scheduler.cpp164
-rw-r--r--indra/llcommon/coro_scheduler.h73
-rwxr-xr-xindra/llcommon/hexdump.h4
-rw-r--r--indra/llcommon/lazyeventapi.cpp4
-rw-r--r--indra/llcommon/llapp.cpp2
-rw-r--r--indra/llcommon/llassettype.cpp17
-rw-r--r--indra/llcommon/llassettype.h2
-rw-r--r--indra/llcommon/llcallbacklist.cpp524
-rw-r--r--indra/llcommon/llcallbacklist.h271
-rw-r--r--indra/llcommon/llcoromutex.h64
-rw-r--r--indra/llcommon/llcoros.cpp139
-rw-r--r--indra/llcommon/llcoros.h108
-rw-r--r--indra/llcommon/lldate.cpp12
-rw-r--r--indra/llcommon/lldate.h8
-rw-r--r--indra/llcommon/lldefs.h53
-rw-r--r--indra/llcommon/lldependencies.h3
-rw-r--r--indra/llcommon/llerror.cpp3
-rw-r--r--indra/llcommon/llerrorlegacy.h32
-rw-r--r--indra/llcommon/lleventapi.cpp20
-rw-r--r--indra/llcommon/lleventapi.h1
-rw-r--r--indra/llcommon/lleventcoro.cpp43
-rw-r--r--indra/llcommon/lleventdispatcher.cpp15
-rw-r--r--indra/llcommon/lleventdispatcher.h14
-rw-r--r--indra/llcommon/lleventfilter.cpp181
-rw-r--r--indra/llcommon/lleventfilter.h147
-rw-r--r--indra/llcommon/llevents.cpp62
-rw-r--r--indra/llcommon/llevents.h181
-rw-r--r--indra/llcommon/lleventtimer.cpp45
-rw-r--r--indra/llcommon/lleventtimer.h85
-rw-r--r--indra/llcommon/llexception.h1
-rw-r--r--indra/llcommon/llformat.h2
-rw-r--r--indra/llcommon/llinstancetracker.h88
-rw-r--r--indra/llcommon/llinttracker.h43
-rw-r--r--indra/llcommon/llleap.cpp49
-rw-r--r--indra/llcommon/llleaplistener.cpp83
-rw-r--r--indra/llcommon/llleaplistener.h27
-rw-r--r--indra/llcommon/lllivefile.cpp2
-rw-r--r--indra/llcommon/llmainthreadtask.h58
-rw-r--r--indra/llcommon/llrefcount.h1
-rw-r--r--indra/llcommon/llrun.h11
-rw-r--r--indra/llcommon/llsdutil.h132
-rw-r--r--indra/llcommon/llsingleton.cpp61
-rw-r--r--indra/llcommon/llsingleton.h84
-rw-r--r--indra/llcommon/llstring.cpp131
-rw-r--r--indra/llcommon/llstring.h111
-rwxr-xr-xindra/llcommon/lockstatic.cpp26
-rw-r--r--indra/llcommon/lockstatic.h79
-rw-r--r--indra/llcommon/lua_function.cpp1670
-rw-r--r--indra/llcommon/lua_function.h619
-rw-r--r--indra/llcommon/lualistener.cpp102
-rw-r--r--indra/llcommon/lualistener.h65
-rw-r--r--indra/llcommon/resultset.cpp96
-rw-r--r--indra/llcommon/resultset.h61
-rw-r--r--indra/llcommon/scope_exit.h34
-rw-r--r--indra/llcommon/stringize.h18
-rwxr-xr-xindra/llcommon/tempset.h41
-rw-r--r--indra/llcommon/tests/StringVec.h10
-rw-r--r--indra/llcommon/tests/llcond_test.cpp75
-rw-r--r--indra/llcommon/tests/llerror_test.cpp119
-rw-r--r--indra/llcommon/tests/lleventcoro_test.cpp81
-rw-r--r--indra/llcommon/tests/lleventdispatcher_test.cpp2
-rw-r--r--indra/llcommon/tests/lleventfilter_test.cpp16
-rw-r--r--indra/llcommon/tests/llleap_test.cpp5
-rw-r--r--indra/llcommon/tests/llmainthreadtask_test.cpp4
-rw-r--r--indra/llcommon/tests/llprocess_test.cpp3
-rw-r--r--indra/llcommon/tests/llrand_test.cpp2
-rw-r--r--indra/llcommon/tests/llsdserialize_test.cpp15
-rw-r--r--indra/llcommon/threadpool.cpp38
-rw-r--r--indra/llcommon/threadpool.h6
-rw-r--r--indra/llcommon/throttle.cpp34
-rw-r--r--indra/llcommon/throttle.h138
-rw-r--r--indra/llcommon/workqueue.cpp7
-rw-r--r--indra/llcommon/workqueue.h33
74 files changed, 5414 insertions, 1184 deletions
diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt
index a504e71340..eaf32f68ea 100644
--- a/indra/llcommon/CMakeLists.txt
+++ b/indra/llcommon/CMakeLists.txt
@@ -16,6 +16,8 @@ include(Tracy)
set(llcommon_SOURCE_FILES
apply.cpp
commoncontrol.cpp
+ coro_scheduler.cpp
+ hbxxh.cpp
indra_constants.cpp
lazyeventapi.cpp
llapp.cpp
@@ -53,8 +55,8 @@ set(llcommon_SOURCE_FILES
llframetimer.cpp
llheartbeat.cpp
llheteromap.cpp
- llinitparam.cpp
llinitdestroyclass.cpp
+ llinitparam.cpp
llinstancetracker.cpp
llkeybind.cpp
llleap.cpp
@@ -64,15 +66,15 @@ set(llcommon_SOURCE_FILES
llmd5.cpp
llmemory.cpp
llmemorystream.cpp
- llmetrics.cpp
llmetricperformancetester.cpp
+ llmetrics.cpp
llmortician.cpp
llmutex.cpp
- llptrto.cpp
llpredicate.cpp
llprocess.cpp
llprocessor.cpp
llprocinfo.cpp
+ llptrto.cpp
llqueuedthread.cpp
llrand.cpp
llrefcount.cpp
@@ -102,9 +104,13 @@ set(llcommon_SOURCE_FILES
lluriparser.cpp
lluuid.cpp
llworkerthread.cpp
- hbxxh.cpp
- u64.cpp
+ lockstatic.cpp
+ lua_function.cpp
+ lualistener.cpp
+ resultset.cpp
threadpool.cpp
+ throttle.cpp
+ u64.cpp
workqueue.cpp
StackWalker.cpp
)
@@ -117,10 +123,12 @@ set(llcommon_HEADER_FILES
chrono.h
classic_callback.h
commoncontrol.h
+ coro_scheduler.h
ctype_workaround.h
fix_macros.h
fsyspath.h
function_types.h
+ hbxxh.h
indra_constants.h
lazyeventapi.h
linden_common.h
@@ -139,6 +147,7 @@ set(llcommon_HEADER_FILES
llcommonutils.h
llcond.h
llcoros.h
+ llcoromutex.h
llcrc.h
llcriticaldamp.h
lldate.h
@@ -155,9 +164,9 @@ set(llcommon_HEADER_FILES
lleventapi.h
lleventcoro.h
lleventdispatcher.h
+ lleventemitter.h
lleventfilter.h
llevents.h
- lleventemitter.h
llexception.h
llfasttimer.h
llfile.h
@@ -174,6 +183,7 @@ set(llcommon_HEADER_FILES
llinitparam.h
llinstancetracker.h
llinstancetrackersubclass.h
+ llinttracker.h
llkeybind.h
llkeythrottle.h
llleap.h
@@ -184,14 +194,12 @@ set(llcommon_HEADER_FILES
llmd5.h
llmemory.h
llmemorystream.h
- llmetrics.h
llmetricperformancetester.h
+ llmetrics.h
llmortician.h
llmutex.h
llnametable.h
llpointer.h
- llprofiler.h
- llprofilercategories.h
llpounceable.h
llpredicate.h
llpreprocessor.h
@@ -199,6 +207,8 @@ set(llcommon_HEADER_FILES
llprocess.h
llprocessor.h
llprocinfo.h
+ llprofiler.h
+ llprofilercategories.h
llptrto.h
llqueuedthread.h
llrand.h
@@ -216,14 +226,14 @@ set(llcommon_HEADER_FILES
llsimplehash.h
llsingleton.h
llstacktrace.h
+ llstaticstringtable.h
+ llstatsaccumulator.h
llstl.h
llstreamqueue.h
llstreamtools.h
llstrider.h
llstring.h
llstringtable.h
- llstaticstringtable.h
- llstatsaccumulator.h
llsys.h
lltempredirect.h
llthread.h
@@ -242,13 +252,17 @@ set(llcommon_HEADER_FILES
lluuid.h
llwin32headers.h
llworkerthread.h
- hbxxh.h
lockstatic.h
+ lua_function.h
+ lualistener.h
+ resultset.h
stdtypes.h
stringize.h
+ tempset.h
threadpool.h
threadpool_fwd.h
threadsafeschedule.h
+ throttle.h
timer.h
tuple.h
u64.h
diff --git a/indra/llcommon/coro_scheduler.cpp b/indra/llcommon/coro_scheduler.cpp
new file mode 100644
index 0000000000..02b9f11333
--- /dev/null
+++ b/indra/llcommon/coro_scheduler.cpp
@@ -0,0 +1,164 @@
+/**
+ * @file coro_scheduler.cpp
+ * @author Nat Goodspeed
+ * @date 2024-08-05
+ * @brief Implementation for llcoro::scheduler.
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Copyright (c) 2024, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "coro_scheduler.h"
+// STL headers
+// std headers
+#include <iomanip>
+// external library headers
+#include <boost/fiber/operations.hpp>
+// other Linden headers
+#include "llcallbacklist.h"
+#include "lldate.h"
+#include "llerror.h"
+
+namespace llcoro
+{
+
+const F64 scheduler::DEFAULT_TIMESLICE{ LL::Timers::DEFAULT_TIMESLICE };
+
+const std::string qname("General");
+
+scheduler::scheduler():
+ // Since use_scheduling_algorithm() must be called before any other
+ // Boost.Fibers operations, we can assume that the calling fiber is in
+ // fact the main fiber.
+ mMainID(boost::this_fiber::get_id()),
+ mStart(LLDate::now().secondsSinceEpoch()),
+ mQueue(LL::WorkQueue::getInstance(qname))
+{}
+
+void scheduler::awakened( boost::fibers::context* ctx) noexcept
+{
+ if (ctx->get_id() == mMainID)
+ {
+ // If the fiber that just came ready is the main fiber, record its
+ // pointer.
+ llassert(! mMainCtx);
+ mMainCtx = ctx;
+ }
+ // Delegate to round_robin::awakened() as usual, even for the main fiber.
+ // This way, as long as other fibers don't take too long, we can just let
+ // normal round_robin processing pass control to the main fiber.
+ super::awakened(ctx);
+}
+
+boost::fibers::context* scheduler::pick_next() noexcept
+{
+ // count calls to pick_next()
+ ++mSwitches;
+ // pick_next() is called when the previous fiber has suspended, and we
+ // need to pick another. Did the previous pick_next() call pick the main
+ // fiber? If so, it's the main fiber that just suspended.
+ auto now = LLDate::now().secondsSinceEpoch();
+ if (mMainRunning)
+ {
+ mMainRunning = false;
+ mMainLast = now;
+ }
+
+ boost::fibers::context* next;
+
+ // When the main fiber is ready, and it's been more than mTimeslice since
+ // the main fiber last ran, it's time to intervene.
+ F64 elapsed(now - mMainLast);
+ if (mMainCtx && elapsed > mTimeslice)
+ {
+ // We claim that the main fiber is not only stored in mMainCtx, but is
+ // also queued (somewhere) in our ready list.
+ llassert(mMainCtx->ready_is_linked());
+ // The usefulness of a doubly-linked list is that, given only a
+ // pointer to an item, we can unlink it.
+ mMainCtx->ready_unlink();
+ // Instead of delegating to round_robin::pick_next() to pop the head
+ // of the queue, override by returning mMainCtx.
+ next = mMainCtx;
+
+ /*------------------------- logging stuff --------------------------*/
+ // Unless this log tag is enabled, don't even bother posting.
+ LL_DEBUGS("LLCoros.scheduler");
+ // This feature is inherently hard to verify. The logging in the
+ // lambda below seems useful, but also seems like a lot of overhead
+ // for a coroutine context switch. Try posting the logging lambda to a
+ // ThreadPool to offload that overhead. However, if this is still
+ // taking an unreasonable amount of context-switch time, this whole
+ // passage could be skipped.
+
+ // Record this event for logging, but push it off to a thread pool to
+ // perform that work. Presumably std::weak_ptr::lock() is cheaper than
+ // WorkQueue::getInstance().
+ LL::WorkQueue::ptr_t queue{ mQueue.lock() };
+ // We probably started before the relevant WorkQueue was created.
+ if (! queue)
+ {
+ // Try again to locate the specified WorkQueue.
+ queue = LL::WorkQueue::getInstance(qname);
+ mQueue = queue;
+ }
+ // Both the lock() call and the getInstance() call might have failed.
+ if (queue)
+ {
+ // Bind values. Do NOT bind 'this' to avoid cross-thread access!
+ // It would be interesting to know from what queue position we
+ // unlinked the main fiber, out of how many in the ready list.
+ // Unfortunately round_robin::rqueue_ is private, not protected,
+ // so we have no access.
+ queue->post(
+ [switches=mSwitches, start=mStart, elapsed, now]
+ ()
+ {
+ U32 runtime(U32(now) - U32(start));
+ U32 minutes(runtime / 60u);
+ U32 seconds(runtime % 60u);
+ // use stringize to avoid lasting side effects to the
+ // logging ostream
+ LL_DEBUGS("LLCoros.scheduler")
+ << "At time "
+ << stringize(minutes, ":", std::setw(2), std::setfill('0'), seconds)
+ << " (" << switches << " switches), coroutines took "
+ << stringize(std::setprecision(4), elapsed)
+ << " sec, main coroutine jumped queue"
+ << LL_ENDL;
+ });
+ }
+ LL_ENDL;
+ /*----------------------- end logging stuff ------------------------*/
+ }
+ else
+ {
+ // Either the main fiber isn't yet ready, or it hasn't yet been
+ // mTimeslice seconds since the last time the main fiber ran. Business
+ // as usual.
+ next = super::pick_next();
+ }
+
+ // super::pick_next() could also have returned the main fiber, which is
+ // why this is a separate test instead of being folded into the override
+ // case above.
+ if (next && next->get_id() == mMainID)
+ {
+ // we're about to resume the main fiber: it's no longer "ready"
+ mMainCtx = nullptr;
+ // instead, it's "running"
+ mMainRunning = true;
+ }
+ return next;
+}
+
+void scheduler::use()
+{
+ boost::fibers::use_scheduling_algorithm<scheduler>();
+}
+
+} // namespace llcoro
diff --git a/indra/llcommon/coro_scheduler.h b/indra/llcommon/coro_scheduler.h
new file mode 100644
index 0000000000..eee2d746b5
--- /dev/null
+++ b/indra/llcommon/coro_scheduler.h
@@ -0,0 +1,73 @@
+/**
+ * @file coro_scheduler.h
+ * @author Nat Goodspeed
+ * @date 2024-08-05
+ * @brief Custom scheduler for viewer's Boost.Fibers (aka coroutines)
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Copyright (c) 2024, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_CORO_SCHEDULER_H)
+#define LL_CORO_SCHEDULER_H
+
+#include "workqueue.h"
+#include <boost/fiber/fiber.hpp>
+#include <boost/fiber/algo/round_robin.hpp>
+
+/**
+ * llcoro::scheduler is specifically intended for the viewer's main thread.
+ * Its role is to ensure that the main coroutine, responsible for UI
+ * operations and coordinating everything else, doesn't get starved by
+ * secondary coroutines -- however many of those there might be.
+ *
+ * The simple boost::fibers::algo::round_robin scheduler could result in
+ * arbitrary time lag between resumptions of the main coroutine. Of course
+ * every well-behaved viewer coroutine must be coded to yield before too much
+ * real time has elapsed, but sheer volume of secondary coroutines could still
+ * consume unreasonable real time before cycling back to the main coroutine.
+ */
+
+namespace llcoro
+{
+
+class scheduler: public boost::fibers::algo::round_robin
+{
+ using super = boost::fibers::algo::round_robin;
+public:
+ // If the main fiber is ready, and it's been at least this long since the
+ // main fiber last ran, jump the main fiber to the head of the queue.
+ static const F64 DEFAULT_TIMESLICE;
+
+ scheduler();
+ void awakened( boost::fibers::context*) noexcept override;
+ boost::fibers::context* pick_next() noexcept override;
+
+ static void use();
+
+private:
+ // This is the fiber::id of the main fiber. We use this to discover
+ // whether the fiber passed to awakened() is in fact the main fiber.
+ boost::fibers::fiber::id mMainID;
+ // This context* is nullptr until awakened() notices that the main fiber
+ // has become ready, at which point it contains the main fiber's context*.
+ boost::fibers::context* mMainCtx{};
+ // Set when pick_next() returns the main fiber.
+ bool mMainRunning{ false };
+ // If it's been at least this long since the last time the main fiber got
+ // control, jump it to the head of the queue.
+ F64 mTimeslice{ DEFAULT_TIMESLICE };
+ // Timestamp as of the last time we suspended the main fiber.
+ F64 mMainLast{ 0 };
+ // Timestamp of start time
+ F64 mStart{ 0 };
+ // count context switches
+ U64 mSwitches{ 0 };
+ // WorkQueue for deferred logging
+ LL::WorkQueue::weak_t mQueue;
+};
+
+} // namespace llcoro
+
+#endif /* ! defined(LL_CORO_SCHEDULER_H) */
diff --git a/indra/llcommon/hexdump.h b/indra/llcommon/hexdump.h
index ab5ba2b16d..4b734426a3 100755
--- a/indra/llcommon/hexdump.h
+++ b/indra/llcommon/hexdump.h
@@ -25,7 +25,7 @@ namespace LL
class hexdump
{
public:
- hexdump(const std::string_view& data):
+ hexdump(std::string_view data):
hexdump(data.data(), data.length())
{}
@@ -66,7 +66,7 @@ private:
class hexmix
{
public:
- hexmix(const std::string_view& data):
+ hexmix(std::string_view data):
mData(data)
{}
diff --git a/indra/llcommon/lazyeventapi.cpp b/indra/llcommon/lazyeventapi.cpp
index 91db0ee4a6..eebed374c3 100644
--- a/indra/llcommon/lazyeventapi.cpp
+++ b/indra/llcommon/lazyeventapi.cpp
@@ -47,7 +47,9 @@ LL::LazyEventAPIBase::~LazyEventAPIBase()
// case, do NOT unregister their name out from under them!
// If this is a static instance being destroyed at process shutdown,
// LLEventPumps will probably have been cleaned up already.
- if (mRegistered && ! LLEventPumps::wasDeleted())
+ // That said, in a test program, LLEventPumps might never have been
+ // constructed to start with.
+ if (mRegistered && LLEventPumps::instanceExists())
{
// unregister the callback to this doomed instance
LLEventPumps::instance().unregisterPumpFactory(mParams.name);
diff --git a/indra/llcommon/llapp.cpp b/indra/llcommon/llapp.cpp
index 08a43983d3..c1dce97632 100644
--- a/indra/llcommon/llapp.cpp
+++ b/indra/llcommon/llapp.cpp
@@ -310,7 +310,7 @@ void LLApp::stepFrame()
{
LLFrameTimer::updateFrameTime();
LLFrameTimer::updateFrameCount();
- LLEventTimer::updateClass();
+ LLCallbackList::instance().callFunctions();
mRunner.run();
}
diff --git a/indra/llcommon/llassettype.cpp b/indra/llcommon/llassettype.cpp
index c09cf7abd2..b0dbc84186 100644
--- a/indra/llcommon/llassettype.cpp
+++ b/indra/llcommon/llassettype.cpp
@@ -30,6 +30,7 @@
#include "lldictionary.h"
#include "llmemory.h"
#include "llsingleton.h"
+#include "llsd.h"
///----------------------------------------------------------------------------
/// Class LLAssetType
@@ -246,3 +247,19 @@ bool LLAssetType::lookupIsAssetIDKnowable(EType asset_type)
}
return false;
}
+
+LLSD LLAssetType::getTypeNames()
+{
+ LLSD type_names;
+ const LLAssetDictionary *dict = LLAssetDictionary::getInstance();
+ for (S32 type = 0; type < AT_COUNT; ++type)
+ {
+ const AssetEntry *entry = dict->lookup(LLAssetType::EType(type));
+ // skip llassettype_bad_lookup
+ if (entry)
+ {
+ type_names.append(entry->mTypeName);
+ }
+ }
+ return type_names;
+}
diff --git a/indra/llcommon/llassettype.h b/indra/llcommon/llassettype.h
index 547c3f4329..17177d81c3 100644
--- a/indra/llcommon/llassettype.h
+++ b/indra/llcommon/llassettype.h
@@ -165,6 +165,8 @@ public:
static bool lookupIsAssetFetchByIDAllowed(EType asset_type); // the asset allows direct download
static bool lookupIsAssetIDKnowable(EType asset_type); // asset data can be known by the viewer
+ static LLSD getTypeNames();
+
static const std::string BADLOOKUP;
protected:
diff --git a/indra/llcommon/llcallbacklist.cpp b/indra/llcommon/llcallbacklist.cpp
index 3d5d30bd90..7b05c25c21 100644
--- a/indra/llcommon/llcallbacklist.cpp
+++ b/indra/llcommon/llcallbacklist.cpp
@@ -24,18 +24,23 @@
* $/LicenseInfo$
*/
+#include "lazyeventapi.h"
#include "llcallbacklist.h"
-#include "lleventtimer.h"
-#include "llerrorlegacy.h"
-
-// Globals
-//
-LLCallbackList gIdleCallbacks;
+#include "llerror.h"
+#include "llexception.h"
+#include "llsdutil.h"
+#include "tempset.h"
+#include <boost/container_hash/hash.hpp>
+#include <iomanip>
+#include <vector>
//
// Member functions
//
+/*****************************************************************************
+* LLCallbackList
+*****************************************************************************/
LLCallbackList::LLCallbackList()
{
// nothing
@@ -45,46 +50,42 @@ LLCallbackList::~LLCallbackList()
{
}
-
-void LLCallbackList::addFunction( callback_t func, void *data)
+LLCallbackList::handle_t LLCallbackList::addFunction( callback_t func, void *data)
{
if (!func)
{
- return;
+ return {};
}
// only add one callback per func/data pair
//
if (containsFunction(func, data))
{
- return;
+ return {};
}
- callback_pair_t t(func, data);
- mCallbackList.push_back(t);
+ auto handle = addFunction([func, data]{ func(data); });
+ mLookup.emplace(callback_pair_t(func, data), handle);
+ return handle;
}
-bool LLCallbackList::containsFunction( callback_t func, void *data)
+LLCallbackList::handle_t LLCallbackList::addFunction( const callable_t& func )
{
- callback_pair_t t(func, data);
- callback_list_t::iterator iter = find(func,data);
- if (iter != mCallbackList.end())
- {
- return true;
- }
- else
- {
- return false;
- }
+ return mCallbackList.connect(func);
}
+bool LLCallbackList::containsFunction( callback_t func, void *data)
+{
+ return mLookup.find(callback_pair_t(func, data)) != mLookup.end();
+}
bool LLCallbackList::deleteFunction( callback_t func, void *data)
{
- callback_list_t::iterator iter = find(func,data);
- if (iter != mCallbackList.end())
+ auto found = mLookup.find(callback_pair_t(func, data));
+ if (found != mLookup.end())
{
- mCallbackList.erase(iter);
+ deleteFunction(found->second);
+ mLookup.erase(found);
return true;
}
else
@@ -93,138 +94,449 @@ bool LLCallbackList::deleteFunction( callback_t func, void *data)
}
}
-inline
-LLCallbackList::callback_list_t::iterator
-LLCallbackList::find(callback_t func, void *data)
+void LLCallbackList::deleteFunction( const handle_t& handle )
{
- callback_pair_t t(func, data);
- return std::find(mCallbackList.begin(), mCallbackList.end(), t);
+ handle.disconnect();
}
void LLCallbackList::deleteAllFunctions()
{
- mCallbackList.clear();
+ mCallbackList = {};
+ mLookup.clear();
}
-
void LLCallbackList::callFunctions()
{
- for (callback_list_t::iterator iter = mCallbackList.begin(); iter != mCallbackList.end(); )
+ mCallbackList();
+}
+
+LLCallbackList::handle_t LLCallbackList::doOnIdleOneTime( const callable_t& func )
+{
+ // connect_extended() passes the connection to the callback
+ return mCallbackList.connect_extended(
+ [func](const handle_t& handle)
+ {
+ handle.disconnect();
+ func();
+ });
+}
+
+LLCallbackList::handle_t LLCallbackList::doOnIdleRepeating( const bool_func_t& func )
+{
+ return mCallbackList.connect_extended(
+ [func](const handle_t& handle)
+ {
+ if (func())
+ {
+ handle.disconnect();
+ }
+ });
+}
+
+/*****************************************************************************
+* LL::Timers
+*****************************************************************************/
+namespace LL
+{
+
+Timers::Timers() {}
+
+// Call a given callable once at specified timestamp.
+Timers::handle_t Timers::scheduleAt(nullary_func_t callable, LLDate::timestamp time)
+{
+ // tick() assumes you want to run periodically until you return true.
+ // Schedule a task that returns true after a single call.
+ return scheduleAtEvery(once(callable), time, 0);
+}
+
+// Call a given callable once after specified interval.
+Timers::handle_t Timers::scheduleAfter(nullary_func_t callable, F32 seconds)
+{
+ return scheduleEvery(once(callable), seconds);
+}
+
+// Call a given callable every specified number of seconds, until it returns true.
+Timers::handle_t Timers::scheduleEvery(bool_func_t callable, F32 seconds)
+{
+ return scheduleAtEvery(callable, now() + seconds, seconds);
+}
+
+Timers::handle_t Timers::scheduleAtEvery(bool_func_t callable,
+ LLDate::timestamp time, F32 interval)
+{
+ // Pick token FIRST to store a self-reference in mQueue's managed node as
+ // well as in mMeta. Pre-increment to distinguish 0 from any live
+ // handle_t.
+ token_t token{ ++mToken };
+ // For the moment, store a default-constructed mQueue handle --
+ // we'll fill in later.
+ auto [iter, inserted] = mMeta.emplace(token,
+ Metadata{ queue_t::handle_type(), time, interval });
+ // It's important that our token is unique.
+ llassert(inserted);
+
+ // Remember whether this is the first entry in mQueue
+ bool first{ mQueue.empty() };
+ auto handle{ mQueue.emplace(callable, token, time) };
+ // Now that we have an mQueue handle_type, store it in mMeta entry.
+ iter->second.mHandle = handle;
+ if (first && ! mLive.connected())
{
- callback_list_t::iterator curiter = iter++;
- curiter->first(curiter->second);
+ // If this is our first entry, register for regular callbacks.
+ mLive = LLCallbackList::instance().doOnIdleRepeating([this]{ return tick(); });
}
+ // Make an Timers::handle_t from token.
+ return { token };
}
-// Shim class to allow arbitrary boost::bind
-// expressions to be run as one-time idle callbacks.
-class OnIdleCallbackOneTime
+bool Timers::isRunning(handle_t timer) const
{
-public:
- OnIdleCallbackOneTime(nullary_func_t callable):
- mCallable(callable)
+ // A default-constructed timer isn't running.
+ // A timer we don't find in mMeta has fired or been canceled.
+ return timer && mMeta.find(timer.token) != mMeta.end();
+}
+
+F32 Timers::timeUntilCall(handle_t timer) const
+{
+ MetaMap::const_iterator found;
+ if ((! timer) || (found = mMeta.find(timer.token)) == mMeta.end())
{
+ return 0.f;
}
- static void onIdle(void *data)
+ else
{
- gIdleCallbacks.deleteFunction(onIdle, data);
- OnIdleCallbackOneTime* self = reinterpret_cast<OnIdleCallbackOneTime*>(data);
- self->call();
- delete self;
+ return narrow(found->second.mTime - now());
}
- void call()
+}
+
+// Cancel a future timer set by scheduleAt(), scheduleAfter(), scheduleEvery()
+bool Timers::cancel(handle_t& timer)
+{
+ // For exception safety, capture and clear timer before canceling.
+ // Once we've canceled this handle, don't retain the live handle.
+ const handle_t ctimer{ timer };
+ timer = handle_t();
+ return cancel(ctimer);
+}
+
+bool Timers::cancel(const handle_t& timer)
+{
+ if (! timer)
{
- mCallable();
+ return false;
}
-private:
- nullary_func_t mCallable;
-};
-void doOnIdleOneTime(nullary_func_t callable)
-{
- OnIdleCallbackOneTime* cb_functor = new OnIdleCallbackOneTime(callable);
- gIdleCallbacks.addFunction(&OnIdleCallbackOneTime::onIdle,cb_functor);
+ // fibonacci_heap documentation does not address the question of what
+ // happens if you call erase() twice with the same handle. Is it a no-op?
+ // Does it invalidate the heap? Is it UB?
+
+ // Nor do we find any documented way to ask whether a given handle still
+ // tracks a valid heap node. That's why we capture all returned handles in
+ // mMeta and validate against that collection. What about the pop()
+ // call in tick()? How to map from the top() value back to the
+ // corresponding handle_t? That's why we store func_at::mToken.
+
+ // fibonacci_heap provides a pair of begin()/end() methods to iterate over
+ // all nodes (NOT in heap order), plus a function to convert from such
+ // iterators to handles. Without mMeta, that would be our only chance
+ // to validate.
+ auto found{ mMeta.find(timer.token) };
+ if (found == mMeta.end())
+ {
+ // we don't recognize this handle -- maybe the timer has already
+ // fired, maybe it was previously canceled.
+ return false;
+ }
+
+ // Funny case: what if the callback directly or indirectly reaches a
+ // cancel() call for its own handle?
+ if (found->second.mRunning)
+ {
+ // tick() has special logic to defer the actual deletion until the
+ // callback has returned
+ found->second.mCancel = true;
+ // this handle does in fact reference a live timer,
+ // which we're going to cancel when we get a chance
+ return true;
+ }
+
+ // Erase from mQueue the handle_type referenced by timer.token.
+ mQueue.erase(found->second.mHandle);
+ // before erasing the mMeta entry
+ mMeta.erase(found);
+ if (mQueue.empty())
+ {
+ // If that was the last active timer, unregister for callbacks.
+ //LLCallbackList::instance().deleteFunction(mLive);
+ // Since we're in the source file that knows the true identity of an
+ // LLCallbackList::handle_t, we don't even need to call instance().
+ mLive.disconnect();
+ }
+ return true;
}
-// Shim class to allow generic boost functions to be run as
-// recurring idle callbacks. Callable should return true when done,
-// false to continue getting called.
-class OnIdleCallbackRepeating
+void Timers::setTimeslice(F32 timeslice)
{
-public:
- OnIdleCallbackRepeating(bool_func_t callable):
- mCallable(callable)
+ if (timeslice < MINIMUM_TIMESLICE)
{
+ // use stringize() so setprecision() affects only the temporary
+ // ostream, not the common logging ostream
+ LL_WARNS("Timers") << "LL::Timers::setTimeslice("
+ << stringize(std::setprecision(4), timeslice)
+ << ") less than "
+ << stringize(std::setprecision(4), MINIMUM_TIMESLICE)
+ << ", ignoring" << LL_ENDL;
}
- // Will keep getting called until the callable returns true.
- static void onIdle(void *data)
+ else
+ {
+ mTimeslice = timeslice;
+ }
+}
+
+bool Timers::tick()
+{
+ // Fetch current time only on entry, even though running some mQueue task
+ // may take long enough that the next one after would become ready. We're
+ // sharing this thread with everything else, and there's a risk we might
+ // starve it if we have a sequence of tasks that take nontrivial time.
+ auto now{ LLDate::now().secondsSinceEpoch() };
+ auto cutoff{ now + mTimeslice };
+
+ // Capture tasks we've processed but that want to be rescheduled.
+ // Defer rescheduling them immediately to avoid getting stuck looping over
+ // a recurring task with a nonpositive interval.
+ std::vector<std::pair<MetaMap::iterator, func_at>> deferred;
+
+ while (! mQueue.empty())
{
- OnIdleCallbackRepeating* self = reinterpret_cast<OnIdleCallbackRepeating*>(data);
- bool done = self->call();
- if (done)
+ auto& top{ mQueue.top() };
+ if (top.mTime > now)
{
- gIdleCallbacks.deleteFunction(onIdle, data);
- delete self;
+ // we've hit an entry that's still in the future:
+ // done with this tick()
+ break;
}
+ if (LLDate::now().secondsSinceEpoch() > cutoff)
+ {
+ // we still have ready tasks, but we've already eaten too much
+ // time this tick() -- defer until next tick()
+ break;
+ }
+
+ // Found a ready task. Look up its corresponding mMeta entry.
+ auto meta{ mMeta.find(top.mToken) };
+ llassert(meta != mMeta.end());
+ bool done;
+ {
+ // Mark our mMeta entry so we don't cancel this timer while its
+ // callback is running, but unmark it even in case of exception.
+ TempSet running(meta->second.mRunning, true);
+ // run the callback and capture its desire to end repetition
+ try
+ {
+ done = top.mFunc();
+ }
+ catch (...)
+ {
+ // Don't crash if a timer callable throws.
+ // But don't continue calling that callable, either.
+ done = true;
+ LOG_UNHANDLED_EXCEPTION("LL::Timers");
+ }
+ } // clear mRunning
+
+ // If mFunc() returned true (all done, stop calling me) or
+ // meta->mCancel (somebody tried to cancel this timer during the
+ // callback call), then we're done: clean up both entries.
+ if (done || meta->second.mCancel)
+ {
+ // remove the mMeta entry referencing this task
+ mMeta.erase(meta);
+ }
+ else
+ {
+ // mFunc returned false, and nobody asked to cancel:
+ // continue calling this task at a future time.
+ meta->second.mTime += meta->second.mInterval;
+ // capture this task to reschedule once we break loop
+ deferred.push_back({meta, top});
+ // update func_at's mTime to match meta's
+ deferred.back().second.mTime = meta->second.mTime;
+ }
+ // Remove the mQueue entry regardless, or we risk stalling the
+ // queue right here if we have a nonpositive interval.
+ mQueue.pop();
}
- bool call()
+
+ // Now reschedule any tasks that need to be rescheduled.
+ for (const auto& [meta, task] : deferred)
{
- return mCallable();
+ auto handle{ mQueue.push(task) };
+ // track this new mQueue handle_type
+ meta->second.mHandle = handle;
}
-private:
- bool_func_t mCallable;
-};
-void doOnIdleRepeating(bool_func_t callable)
-{
- OnIdleCallbackRepeating* cb_functor = new OnIdleCallbackRepeating(callable);
- gIdleCallbacks.addFunction(&OnIdleCallbackRepeating::onIdle,cb_functor);
+ // If, after all the twiddling above, our queue ended up empty,
+ // stop calling every tick.
+ return mQueue.empty();
}
-class NullaryFuncEventTimer: public LLEventTimer
+/*****************************************************************************
+* TimersListener
+*****************************************************************************/
+
+class TimersListener: public LLEventAPI
{
public:
- NullaryFuncEventTimer(nullary_func_t callable, F32 seconds):
- LLEventTimer(seconds),
- mCallable(callable)
+ TimersListener(const LazyEventAPIParams& params): LLEventAPI(params) {}
+
+ // Forbid a script from requesting callbacks too quickly.
+ static constexpr LLSD::Real MINTIMER{ 0.010 };
+
+ void scheduleAfter(const LLSD& params);
+ void scheduleEvery(const LLSD& params);
+ LLSD cancel(const LLSD& params);
+ LLSD isRunning(const LLSD& params);
+ LLSD timeUntilCall(const LLSD& params);
+
+private:
+ // We use the incoming reqid to distinguish different timers -- but reqid
+ // by itself is not unique! Each reqid is local to a calling script.
+ // Distinguish scripts by reply-pump name, then reqid within script.
+ // "Additional specializations for std::pair and the standard container
+ // types, as well as utility functions to compose hashes are available in
+ // boost::hash."
+ // https://en.cppreference.com/w/cpp/utility/hash
+ using HandleKey = std::pair<LLSD::String, LLSD::Integer>;
+ using HandleMap = std::unordered_map<HandleKey, Timers::temp_handle_t,
+ boost::hash<HandleKey>>;
+ HandleMap mHandles;
+};
+
+void TimersListener::scheduleAfter(const LLSD& params)
+{
+ // Timer creation functions respond immediately with the reqid of the
+ // created timer, as well as later when the timer fires. That lets the
+ // requester invoke cancel, isRunning or timeUntilCall.
+ Response response(LLSD(), params);
+ LLSD::Real after{ params["after"] };
+ if (after < MINTIMER)
{
+ return response.error(stringize("after must be at least ", MINTIMER));
}
-private:
- bool tick()
+ HandleKey key{ params["reply"], params["reqid"] };
+ mHandles.emplace(
+ key,
+ Timers::instance().scheduleAfter(
+ [this, params, key]
+ {
+ // we don't need any content save for the "reqid"
+ sendReply({}, params);
+ // ditch mHandles entry
+ mHandles.erase(key);
+ },
+ narrow(after)));
+}
+
+void TimersListener::scheduleEvery(const LLSD& params)
+{
+ // Timer creation functions respond immediately with the reqid of the
+ // created timer, as well as later when the timer fires. That lets the
+ // requester invoke cancel, isRunning or timeUntilCall.
+ Response response(LLSD(), params);
+ LLSD::Real every{ params["every"] };
+ if (every < MINTIMER)
{
- mCallable();
- return true;
+ return response.error(stringize("every must be at least ", MINTIMER));
}
- nullary_func_t mCallable;
-};
+ mHandles.emplace(
+ HandleKey{ params["reply"], params["reqid"] },
+ Timers::instance().scheduleEvery(
+ [params, i=0]() mutable
+ {
+ // we don't need any content save for the "reqid"
+ sendReply(llsd::map("i", i++), params);
+ // we can't use a handshake -- always keep the ball rolling
+ return false;
+ },
+ narrow(every)));
+}
-// Call a given callable once after specified interval.
-void doAfterInterval(nullary_func_t callable, F32 seconds)
+LLSD TimersListener::cancel(const LLSD& params)
{
- new NullaryFuncEventTimer(callable, seconds);
+ auto found{ mHandles.find({params["reply"], params["id"]}) };
+ bool ok = false;
+ if (found != mHandles.end())
+ {
+ ok = true;
+ Timers::instance().cancel(found->second);
+ mHandles.erase(found);
+ }
+ return llsd::map("ok", ok);
}
-class BoolFuncEventTimer: public LLEventTimer
+LLSD TimersListener::isRunning(const LLSD& params)
{
-public:
- BoolFuncEventTimer(bool_func_t callable, F32 seconds):
- LLEventTimer(seconds),
- mCallable(callable)
+ auto found{ mHandles.find({params["reply"], params["id"]}) };
+ bool running = false;
+ if (found != mHandles.end())
{
+ running = Timers::instance().isRunning(found->second);
}
-private:
- bool tick()
+ return llsd::map("running", running);
+}
+
+LLSD TimersListener::timeUntilCall(const LLSD& params)
+{
+ auto found{ mHandles.find({params["reply"], params["id"]}) };
+ bool ok = false;
+ LLSD::Real remaining = 0;
+ if (found != mHandles.end())
{
- return mCallable();
+ ok = true;
+ remaining = Timers::instance().timeUntilCall(found->second);
}
+ return llsd::map("ok", ok, "remaining", remaining);
+}
- bool_func_t mCallable;
+class TimersRegistrar: public LazyEventAPI<TimersListener>
+{
+ using super = LazyEventAPI<TimersListener>;
+ using super::listener;
+
+public:
+ TimersRegistrar():
+ super("Timers", "Provide access to viewer timer functionality.")
+ {
+ add("scheduleAfter",
+R"-(Create a timer with ID "reqid". Post response after "after" seconds.)-",
+ &listener::scheduleAfter,
+ llsd::map("reqid", LLSD::Integer(), "after", LLSD::Real()));
+ add("scheduleEvery",
+R"-(Create a timer with ID "reqid". Post response every "every" seconds
+until cancel().)-",
+ &listener::scheduleEvery,
+ llsd::map("reqid", LLSD::Integer(), "every", LLSD::Real()));
+ add("cancel",
+R"-(Cancel the timer with ID "id". Respond "ok"=true if "id" identifies
+a live timer.)-",
+ &listener::cancel,
+ llsd::map("reqid", LLSD::Integer(), "id", LLSD::Integer()));
+ add("isRunning",
+R"-(Query the timer with ID "id": respond "running"=true if "id" identifies
+a live timer.)-",
+ &listener::isRunning,
+ llsd::map("reqid", LLSD::Integer(), "id", LLSD::Integer()));
+ add("timeUntilCall",
+R"-(Query the timer with ID "id": if "id" identifies a live timer, respond
+"ok"=true, "remaining"=seconds with the time left before timer expiry;
+otherwise "ok"=false, "remaining"=0.)-",
+ &listener::timeUntilCall,
+ llsd::map("reqid", LLSD::Integer()));
+ }
};
+static TimersRegistrar registrar;
-// Call a given callable every specified number of seconds, until it returns true.
-void doPeriodically(bool_func_t callable, F32 seconds)
-{
- new BoolFuncEventTimer(callable, seconds);
-}
+} // namespace LL
diff --git a/indra/llcommon/llcallbacklist.h b/indra/llcommon/llcallbacklist.h
index d6c415f7c5..fb4696188a 100644
--- a/indra/llcommon/llcallbacklist.h
+++ b/indra/llcommon/llcallbacklist.h
@@ -27,53 +27,282 @@
#ifndef LL_LLCALLBACKLIST_H
#define LL_LLCALLBACKLIST_H
+#include "lldate.h"
+#include "llsingleton.h"
#include "llstl.h"
-#include <boost/function.hpp>
-#include <list>
+#include <boost/container_hash/hash.hpp>
+#include <boost/heap/fibonacci_heap.hpp>
+#include <boost/signals2.hpp>
+#include <functional>
+#include <unordered_map>
-class LLCallbackList
+/*****************************************************************************
+* LLCallbackList: callbacks every idle tick (every callFunctions() call)
+*****************************************************************************/
+class LLCallbackList: public LLSingleton<LLCallbackList>
{
+ LLSINGLETON(LLCallbackList);
public:
typedef void (*callback_t)(void*);
- typedef std::pair< callback_t,void* > callback_pair_t;
- // NOTE: It is confirmed that we DEPEND on the order provided by using a list :(
- //
- typedef std::list< callback_pair_t > callback_list_t;
+ typedef boost::signals2::signal<void()> callback_list_t;
+ typedef callback_list_t::slot_type callable_t;
+ typedef boost::signals2::connection handle_t;
+ typedef boost::signals2::scoped_connection temp_handle_t;
+ typedef std::function<bool ()> bool_func_t;
- LLCallbackList();
~LLCallbackList();
- void addFunction( callback_t func, void *data = NULL ); // register a callback, which will be called as func(data)
+ handle_t addFunction( callback_t func, void *data = NULL ); // register a callback, which will be called as func(data)
+ handle_t addFunction( const callable_t& func );
bool containsFunction( callback_t func, void *data = NULL ); // true if list already contains the function/data pair
bool deleteFunction( callback_t func, void *data = NULL ); // removes the first instance of this function/data pair from the list, false if not found
- void callFunctions(); // calls all functions
+ void deleteFunction( const handle_t& handle );
+ void callFunctions(); // calls all functions
void deleteAllFunctions();
+ handle_t doOnIdleOneTime( const callable_t& func );
+ handle_t doOnIdleRepeating( const bool_func_t& func );
+ bool isRunning(const handle_t& handle) const { return handle.connected(); };
+
static void test();
protected:
-
- inline callback_list_t::iterator find(callback_t func, void *data);
-
callback_list_t mCallbackList;
+
+ // "Additional specializations for std::pair and the standard container
+ // types, as well as utility functions to compose hashes are available in
+ // boost::hash."
+ // https://en.cppreference.com/w/cpp/utility/hash
+ typedef std::pair< callback_t,void* > callback_pair_t;
+ typedef std::unordered_map<callback_pair_t, handle_t,
+ boost::hash<callback_pair_t>> lookup_table;
+ lookup_table mLookup;
};
-typedef boost::function<void ()> nullary_func_t;
-typedef boost::function<bool ()> bool_func_t;
+/*-------------------- legacy names in global namespace --------------------*/
+#define gIdleCallbacks (LLCallbackList::instance())
+
+using nullary_func_t = LLCallbackList::callable_t;
+using bool_func_t = LLCallbackList::bool_func_t;
// Call a given callable once in idle loop.
-void doOnIdleOneTime(nullary_func_t callable);
+inline
+LLCallbackList::handle_t doOnIdleOneTime(nullary_func_t callable)
+{
+ return gIdleCallbacks.doOnIdleOneTime(callable);
+}
// Repeatedly call a callable in idle loop until it returns true.
-void doOnIdleRepeating(bool_func_t callable);
+inline
+LLCallbackList::handle_t doOnIdleRepeating(bool_func_t callable)
+{
+ return gIdleCallbacks.doOnIdleRepeating(callable);
+}
+
+/*****************************************************************************
+* LL::Timers: callbacks at some future time
+*****************************************************************************/
+namespace LL
+{
+
+class Timers: public LLSingleton<Timers>
+{
+ LLSINGLETON(Timers);
+
+ using token_t = U32;
+
+ // Define a struct for our priority queue entries, instead of using
+ // a tuple, because we need to define the comparison operator.
+ struct func_at
+ {
+ // callback to run when this timer fires
+ bool_func_t mFunc;
+ // key to look up metadata in mHandles
+ token_t mToken;
+ // time at which this timer is supposed to fire
+ LLDate::timestamp mTime;
+
+ func_at(const bool_func_t& func, token_t token, LLDate::timestamp tm):
+ mFunc(func),
+ mToken(token),
+ mTime(tm)
+ {}
+
+ friend bool operator<(const func_at& lhs, const func_at& rhs)
+ {
+ // use greater-than because we want fibonacci_heap to select the
+ // EARLIEST time as the top()
+ return lhs.mTime > rhs.mTime;
+ }
+ };
+
+ // Accept default stable<false>: when two funcs have the same timestamp,
+ // we don't care in what order they're called.
+ // Specify constant_time_size<false>: we don't need to optimize the size()
+ // method, iow we don't need to store and maintain a count of entries.
+ typedef boost::heap::fibonacci_heap<func_at, boost::heap::constant_time_size<false>>
+ queue_t;
+
+public:
+ // If tasks that come ready during a given tick() take longer than this,
+ // defer any subsequent ready tasks to a future tick() call.
+ static constexpr F32 DEFAULT_TIMESLICE{ 0.005f };
+ // Setting timeslice to be less than MINIMUM_TIMESLICE could lock up
+ // Timers processing, causing it to believe it's exceeded the allowable
+ // time every tick before processing ANY queue items.
+ static constexpr F32 MINIMUM_TIMESLICE{ 0.001f };
+
+ class handle_t
+ {
+ private:
+ friend class Timers;
+ token_t token;
+ public:
+ handle_t(token_t token=0): token(token) {}
+ bool operator==(const handle_t& rhs) const { return this->token == rhs.token; }
+ explicit operator bool() const { return bool(token); }
+ bool operator!() const { return ! bool(*this); }
+ };
+ // Call a given callable once at specified timestamp.
+ handle_t scheduleAt(nullary_func_t callable, LLDate::timestamp time);
+
+ // Call a given callable once after specified interval.
+ handle_t scheduleAfter(nullary_func_t callable, F32 seconds);
+
+ // Call a given callable every specified number of seconds, until it returns true.
+ handle_t scheduleEvery(bool_func_t callable, F32 seconds);
+
+ // test whether specified handle is still live
+ bool isRunning(handle_t timer) const;
+ // check remaining time
+ F32 timeUntilCall(handle_t timer) const;
+
+ // Cancel a future timer set by scheduleAt(), scheduleAfter(), scheduleEvery().
+ // Return true if and only if the handle corresponds to a live timer.
+ bool cancel(const handle_t& timer);
+ // If we're canceling a non-const handle_t, also clear it so we need not
+ // cancel again.
+ bool cancel(handle_t& timer);
+
+ F32 getTimeslice() const { return mTimeslice; }
+ void setTimeslice(F32 timeslice);
+
+ // Store a handle_t returned by scheduleAt(), scheduleAfter() or
+ // scheduleEvery() in a temp_handle_t to cancel() automatically on
+ // destruction of the temp_handle_t.
+ class temp_handle_t
+ {
+ public:
+ temp_handle_t() = default;
+ temp_handle_t(const handle_t& hdl): mHandle(hdl) {}
+ temp_handle_t(const temp_handle_t&) = delete;
+ temp_handle_t(temp_handle_t&&) = default;
+ temp_handle_t& operator=(const handle_t& hdl)
+ {
+ // initializing a new temp_handle_t, then swapping it into *this,
+ // takes care of destroying any previous mHandle
+ temp_handle_t replacement(hdl);
+ swap(replacement);
+ return *this;
+ }
+ temp_handle_t& operator=(const temp_handle_t&) = delete;
+ temp_handle_t& operator=(temp_handle_t&&) = default;
+ ~temp_handle_t()
+ {
+ cancel();
+ }
+
+ // temp_handle_t should be usable wherever handle_t is
+ operator handle_t() const { return mHandle; }
+ // If we're dealing with a non-const temp_handle_t, pass a reference
+ // to our handle_t member (e.g. to Timers::cancel()).
+ operator handle_t&() { return mHandle; }
+
+ // For those in the know, provide a cancel() method of our own that
+ // avoids Timers::instance() lookup when mHandle isn't live.
+ bool cancel()
+ {
+ if (! mHandle)
+ {
+ return false;
+ }
+ else
+ {
+ return Timers::instance().cancel(mHandle);
+ }
+ }
+
+ void swap(temp_handle_t& other) noexcept
+ {
+ std::swap(this->mHandle, other.mHandle);
+ }
+
+ private:
+ handle_t mHandle;
+ };
+
+private:
+ handle_t scheduleAtEvery(bool_func_t callable, LLDate::timestamp time, F32 interval);
+ LLDate::timestamp now() const { return LLDate::now().secondsSinceEpoch(); }
+ // wrap a nullary_func_t with a bool_func_t that will only execute once
+ bool_func_t once(nullary_func_t callable)
+ {
+ return [callable]
+ {
+ callable();
+ return true;
+ };
+ }
+ bool tick();
+
+ // NOTE: We don't lock our data members because it doesn't make sense to
+ // register cross-thread callbacks. If we start wanting to use Timers on
+ // threads other than the main thread, it would make more sense to make
+ // our data members thread_local than to lock them.
+
+ // the heap aka priority queue
+ queue_t mQueue;
+
+ // metadata about a given task
+ struct Metadata
+ {
+ // handle to mQueue entry
+ queue_t::handle_type mHandle;
+ // time at which this timer is supposed to fire
+ LLDate::timestamp mTime;
+ // interval at which this timer is supposed to fire repeatedly
+ F32 mInterval{ 0 };
+ // mFunc is currently running: don't delete this entry
+ bool mRunning{ false };
+ // cancel() was called while mFunc was running: deferred cancel
+ bool mCancel{ false };
+ };
+
+ using MetaMap = std::unordered_map<token_t, Metadata>;
+ MetaMap mMeta;
+ token_t mToken{ 0 };
+ // While mQueue is non-empty, register for regular callbacks.
+ LLCallbackList::temp_handle_t mLive;
+ F32 mTimeslice{ DEFAULT_TIMESLICE };
+};
+
+} // namespace LL
+
+/*-------------------- legacy names in global namespace --------------------*/
// Call a given callable once after specified interval.
-void doAfterInterval(nullary_func_t callable, F32 seconds);
+inline
+LL::Timers::handle_t doAfterInterval(nullary_func_t callable, F32 seconds)
+{
+ return LL::Timers::instance().scheduleAfter(callable, seconds);
+}
// Call a given callable every specified number of seconds, until it returns true.
-void doPeriodically(bool_func_t callable, F32 seconds);
-
-extern LLCallbackList gIdleCallbacks;
+inline
+LL::Timers::handle_t doPeriodically(bool_func_t callable, F32 seconds)
+{
+ return LL::Timers::instance().scheduleEvery(callable, seconds);
+}
#endif
diff --git a/indra/llcommon/llcoromutex.h b/indra/llcommon/llcoromutex.h
new file mode 100644
index 0000000000..c0ceac4b22
--- /dev/null
+++ b/indra/llcommon/llcoromutex.h
@@ -0,0 +1,64 @@
+/**
+ * @file llcoromutex.h
+ * @author Nat Goodspeed
+ * @date 2024-09-04
+ * @brief Coroutine-aware synchronization primitives
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Copyright (c) 2024, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_LLCOROMUTEX_H)
+#define LL_LLCOROMUTEX_H
+
+#include "mutex.h"
+#include <boost/fiber/future/promise.hpp>
+#include <boost/fiber/future/future.hpp>
+
+// e.g. #include LLCOROS_MUTEX_HEADER
+#define LLCOROS_MUTEX_HEADER <boost/fiber/mutex.hpp>
+#define LLCOROS_RMUTEX_HEADER <boost/fiber/recursive_mutex.hpp>
+#define LLCOROS_CONDVAR_HEADER <boost/fiber/condition_variable.hpp>
+
+namespace boost {
+ namespace fibers {
+ class mutex;
+ class recursive_mutex;
+ enum class cv_status;
+ class condition_variable;
+ }
+}
+
+namespace llcoro
+{
+
+/**
+ * Aliases for promise and future. An older underlying future implementation
+ * required us to wrap future; that's no longer needed. However -- if it's
+ * important to restore kill() functionality, we might need to provide a
+ * proxy, so continue using the aliases.
+ */
+template <typename T>
+using Promise = boost::fibers::promise<T>;
+template <typename T>
+using Future = boost::fibers::future<T>;
+template <typename T>
+inline
+static Future<T> getFuture(Promise<T>& promise) { return promise.get_future(); }
+
+// use mutex, lock, condition_variable suitable for coroutines
+using Mutex = boost::fibers::mutex;
+using RMutex = boost::fibers::recursive_mutex;
+// With C++17, LockType is deprecated: at this point we can directly
+// declare 'std::unique_lock lk(some_mutex)' without explicitly stating
+// the mutex type. Sadly, making LockType an alias template for
+// std::unique_lock doesn't work the same way: Class Template Argument
+// Deduction only works for class templates, not alias templates.
+using LockType = std::unique_lock<Mutex>;
+using cv_status = boost::fibers::cv_status;
+using ConditionVariable = boost::fibers::condition_variable;
+
+} // namespace llcoro
+
+#endif /* ! defined(LL_LLCOROMUTEX_H) */
diff --git a/indra/llcommon/llcoros.cpp b/indra/llcommon/llcoros.cpp
index 1539b48bd3..0b46802295 100644
--- a/indra/llcommon/llcoros.cpp
+++ b/indra/llcommon/llcoros.cpp
@@ -51,11 +51,12 @@
#endif
// other Linden headers
#include "llapp.h"
-#include "lltimer.h"
-#include "llevents.h"
#include "llerror.h"
-#include "stringize.h"
+#include "llevents.h"
#include "llexception.h"
+#include "llsdutil.h"
+#include "lltimer.h"
+#include "stringize.h"
#if LL_WINDOWS
#include <excpt.h>
@@ -64,12 +65,8 @@
// static
bool LLCoros::on_main_coro()
{
- if (!LLCoros::instanceExists() || LLCoros::getName().empty())
- {
- return true;
- }
-
- return false;
+ return (!LLCoros::instanceExists() ||
+ LLCoros::getName().empty());
}
// static
@@ -79,7 +76,7 @@ bool LLCoros::on_main_thread_main_coro()
}
// static
-LLCoros::CoroData& LLCoros::get_CoroData(const std::string& caller)
+LLCoros::CoroData& LLCoros::get_CoroData(const std::string&)
{
CoroData* current{ nullptr };
// be careful about attempted accesses in the final throes of app shutdown
@@ -111,7 +108,7 @@ LLCoros::coro::id LLCoros::get_self()
//static
void LLCoros::set_consuming(bool consuming)
{
- CoroData& data(get_CoroData("set_consuming()"));
+ auto& data(get_CoroData("set_consuming()"));
// DO NOT call this on the main() coroutine.
llassert_always(! data.mName.empty());
data.mConsuming = consuming;
@@ -145,6 +142,15 @@ LLCoros::LLCoros():
// points to it. So initialize it with a no-op deleter.
mCurrent{ [](CoroData*){} }
{
+ auto& llapp{ LLEventPumps::instance().obtain("LLApp") };
+ if (llapp.getListener("LLCoros") == LLBoundListener())
+ {
+ // chain our "LLCoros" pump onto "LLApp" pump: echo events posted to "LLApp"
+ mConn = llapp.listen(
+ "LLCoros",
+ [](const LLSD& event)
+ { return LLEventPumps::instance().obtain("LLCoros").post(event); });
+ }
}
LLCoros::~LLCoros()
@@ -194,26 +200,26 @@ std::string LLCoros::generateDistinctName(const std::string& prefix) const
// Until we find an unused name, append a numeric suffix for uniqueness.
while (CoroData::getInstance(name))
{
- name = STRINGIZE(prefix << unique++);
+ name = stringize(prefix, unique++);
}
return name;
}
-/*==========================================================================*|
-bool LLCoros::kill(const std::string& name)
+bool LLCoros::killreq(const std::string& name)
{
- CoroMap::iterator found = mCoros.find(name);
- if (found == mCoros.end())
+ auto found = CoroData::getInstance(name);
+ if (! found)
{
return false;
}
- // Because this is a boost::ptr_map, erasing the map entry also destroys
- // the referenced heap object, in this case the boost::coroutine object,
- // which will terminate the coroutine.
- mCoros.erase(found);
+ // Next time the subject coroutine calls checkStop(), make it terminate.
+ found->mKilledBy = getName();
+ // But if it's waiting for something, notify anyone in a position to poke
+ // it.
+ LLEventPumps::instance().obtain("LLCoros").post(
+ llsd::map("status", "killreq", "coro", name));
return true;
}
-|*==========================================================================*/
//static
std::string LLCoros::getName()
@@ -224,7 +230,7 @@ std::string LLCoros::getName()
//static
std::string LLCoros::logname()
{
- LLCoros::CoroData& data(get_CoroData("logname()"));
+ auto& data(get_CoroData("logname()"));
return data.mName.empty()? data.getKey() : data.mName;
}
@@ -277,7 +283,7 @@ std::string LLCoros::launch(const std::string& prefix, const callable_t& callabl
// std::allocator_arg is a flag to indicate that the following argument is
// a StackAllocator.
// protected_fixedsize_stack sets a guard page past the end of the new
- // stack so that stack underflow will result in an access violation
+ // stack so that stack overflow will result in an access violation
// instead of weird, subtle, possibly undiagnosed memory stomps.
try
@@ -362,10 +368,12 @@ void LLCoros::toplevel(std::string name, callable_t callable)
// set it as current
mCurrent.reset(&corodata);
+ LL_DEBUGS("LLCoros") << "entering " << name << LL_ENDL;
// run the code the caller actually wants in the coroutine
try
{
sehandle(callable);
+ LL_DEBUGS("LLCoros") << "done " << name << LL_ENDL;
}
catch (const Stop& exc)
{
@@ -377,7 +385,7 @@ void LLCoros::toplevel(std::string name, callable_t callable)
// Any uncaught exception derived from LLContinueError will be caught
// here and logged. This coroutine will terminate but the rest of the
// viewer will carry on.
- LOG_UNHANDLED_EXCEPTION(STRINGIZE("coroutine " << name));
+ LOG_UNHANDLED_EXCEPTION("coroutine " + name);
}
catch (...)
{
@@ -390,15 +398,24 @@ void LLCoros::toplevel(std::string name, callable_t callable)
}
//static
-void LLCoros::checkStop()
+void LLCoros::checkStop(callable_t cleanup)
{
+ // don't replicate this 'if' test throughout the code below
+ if (! cleanup)
+ {
+ cleanup = {[](){}}; // hey, look, I'm coding in Haskell!
+ }
+
if (wasDeleted())
{
+ cleanup();
LLTHROW(Shutdown("LLCoros was deleted"));
}
- // do this AFTER the check above, because getName() depends on
- // get_CoroData(), which depends on the local_ptr in our instance().
- if (getName().empty())
+
+ // do this AFTER the check above, because get_CoroData() depends on the
+ // local_ptr in our instance().
+ auto& data(get_CoroData("checkStop()"));
+ if (data.mName.empty())
{
// Our Stop exception and its subclasses are intended to stop loitering
// coroutines. Don't throw it from the main coroutine.
@@ -406,19 +423,80 @@ void LLCoros::checkStop()
}
if (LLApp::isStopped())
{
+ cleanup();
LLTHROW(Stopped("viewer is stopped"));
}
if (! LLApp::isRunning())
{
+ cleanup();
LLTHROW(Stopping("viewer is stopping"));
}
+ if (! data.mKilledBy.empty())
+ {
+ // Someone wants to kill this coroutine
+ cleanup();
+ LLTHROW(Killed(stringize("coroutine ", data.mName, " killed by ", data.mKilledBy)));
+ }
+}
+
+LLBoundListener LLCoros::getStopListener(const std::string& caller, LLVoidListener cleanup)
+{
+ if (! cleanup)
+ return {};
+
+ // This overload only responds to viewer shutdown.
+ return LLEventPumps::instance().obtain("LLCoros")
+ .listen(
+ LLEventPump::inventName(caller),
+ [cleanup](const LLSD& event)
+ {
+ auto status{ event["status"].asString() };
+ if (status != "running" && status != "killreq")
+ {
+ cleanup(event);
+ }
+ return false;
+ });
+}
+
+LLBoundListener LLCoros::getStopListener(const std::string& caller,
+ const std::string& cnsmr,
+ LLVoidListener cleanup)
+{
+ if (! cleanup)
+ return {};
+
+ std::string consumer{cnsmr};
+ if (consumer.empty())
+ {
+ consumer = getName();
+ }
+
+ // This overload responds to viewer shutdown and to killreq(consumer).
+ return LLEventPumps::instance().obtain("LLCoros")
+ .listen(
+ LLEventPump::inventName(caller),
+ [consumer, cleanup](const LLSD& event)
+ {
+ auto status{ event["status"].asString() };
+ if (status == "killreq")
+ {
+ if (event["coro"].asString() == consumer)
+ {
+ cleanup(event);
+ }
+ }
+ else if (status != "running")
+ {
+ cleanup(event);
+ }
+ return false;
+ });
}
LLCoros::CoroData::CoroData(const std::string& name):
LLInstanceTracker<CoroData, std::string>(name),
mName(name),
- // don't consume events unless specifically directed
- mConsuming(false),
mCreationTime(LLTimer::getTotalSeconds())
{
}
@@ -431,7 +509,6 @@ LLCoros::CoroData::CoroData(int n):
// empty string as its visible name because some consumers test for that.
LLInstanceTracker<CoroData, std::string>("main" + stringize(n)),
mName(),
- mConsuming(false),
mCreationTime(LLTimer::getTotalSeconds())
{
}
diff --git a/indra/llcommon/llcoros.h b/indra/llcommon/llcoros.h
index c3820ae987..0291d7f1d9 100644
--- a/indra/llcommon/llcoros.h
+++ b/indra/llcommon/llcoros.h
@@ -29,30 +29,16 @@
#if ! defined(LL_LLCOROS_H)
#define LL_LLCOROS_H
+#include "llcoromutex.h"
+#include "llevents.h"
#include "llexception.h"
-#include <boost/fiber/fss.hpp>
-#include <boost/fiber/future/future.hpp>
-#include <boost/fiber/future/promise.hpp>
-#include <boost/fiber/recursive_mutex.hpp>
-#include "mutex.h"
-#include "llsingleton.h"
#include "llinstancetracker.h"
-#include <boost/function.hpp>
-#include <string>
+#include "llsingleton.h"
+#include <boost/fiber/fss.hpp>
#include <exception>
+#include <functional>
#include <queue>
-
-// e.g. #include LLCOROS_MUTEX_HEADER
-#define LLCOROS_MUTEX_HEADER <boost/fiber/mutex.hpp>
-#define LLCOROS_CONDVAR_HEADER <boost/fiber/condition_variable.hpp>
-
-namespace boost {
- namespace fibers {
- class mutex;
- enum class cv_status;
- class condition_variable;
- }
-}
+#include <string>
/**
* Registry of named Boost.Coroutine instances
@@ -112,7 +98,7 @@ public:
/// stuck with the term "coroutine."
typedef boost::fibers::fiber coro;
/// Canonical callable type
- typedef boost::function<void()> callable_t;
+ typedef std::function<void()> callable_t;
/**
* Create and start running a new coroutine with specified name. The name
@@ -154,13 +140,13 @@ public:
std::string launch(const std::string& prefix, const callable_t& callable);
/**
- * Abort a running coroutine by name. Normally, when a coroutine either
+ * Ask the named coroutine to abort. Normally, when a coroutine either
* runs to completion or terminates with an exception, LLCoros quietly
* cleans it up. This is for use only when you must explicitly interrupt
* one prematurely. Returns @c true if the specified name was found and
* still running at the time.
*/
-// bool kill(const std::string& name);
+ bool killreq(const std::string& name);
/**
* From within a coroutine, look up the (tweaked) name string by which
@@ -262,15 +248,21 @@ public:
/// thrown by checkStop()
// It may sound ironic that Stop is derived from LLContinueError, but the
// point is that LLContinueError is the category of exception that should
- // not immediately crash the viewer. Stop and its subclasses are to notify
- // coroutines that the viewer intends to shut down. The expected response
- // is to terminate the coroutine, rather than abort the viewer.
+ // not immediately crash the viewer. Stop and its subclasses are to tell
+ // coroutines to terminate, e.g. because the viewer is shutting down. We
+ // do not want any such exception to crash the viewer.
struct Stop: public LLContinueError
{
Stop(const std::string& what): LLContinueError(what) {}
};
- /// early stages
+ /// someone wants to kill this specific coroutine
+ struct Killed: public Stop
+ {
+ Killed(const std::string& what): Stop(what) {}
+ };
+
+ /// early shutdown stages
struct Stopping: public Stop
{
Stopping(const std::string& what): Stop(what) {}
@@ -289,34 +281,50 @@ public:
};
/// Call this intermittently if there's a chance your coroutine might
- /// continue running into application shutdown. Throws Stop if LLCoros has
- /// been cleaned up.
- static void checkStop();
+ /// still be running at application shutdown. Throws one of the Stop
+ /// subclasses if the caller needs to terminate. Pass a cleanup function
+ /// if you need to execute that cleanup before terminating.
+ /// Of course, if your cleanup function throws, that will be the exception
+ /// propagated by checkStop().
+ static void checkStop(callable_t cleanup={});
+
+ /// Call getStopListener() at the source end of a queue, promise or other
+ /// resource on which coroutines will wait, so that shutdown can wake up
+ /// consuming coroutines. @a caller should distinguish who's calling. The
+ /// passed @a cleanup function must close the queue, break the promise or
+ /// otherwise cause waiting consumers to wake up in an abnormal way. It's
+ /// advisable to store the returned LLBoundListener in an
+ /// LLTempBoundListener, or otherwise arrange to disconnect it.
+ static LLBoundListener getStopListener(const std::string& caller, LLVoidListener cleanup);
+
+ /// This getStopListener() overload is like the two-argument one, for use
+ /// when we know the name of the only coroutine that will wait on the
+ /// resource in question. Pass @a consumer as the empty string if the
+ /// consumer coroutine is the same as the calling coroutine. Unlike the
+ /// two-argument getStopListener(), this one also responds to
+ /// killreq(target).
+ static LLBoundListener getStopListener(const std::string& caller,
+ const std::string& consumer,
+ LLVoidListener cleanup);
/**
- * Aliases for promise and future. An older underlying future implementation
- * required us to wrap future; that's no longer needed. However -- if it's
- * important to restore kill() functionality, we might need to provide a
- * proxy, so continue using the aliases.
+ * LLCoros aliases for promise and future, for backwards compatibility.
+ * These have been hoisted out to the llcoro namespace.
*/
template <typename T>
- using Promise = boost::fibers::promise<T>;
+ using Promise = llcoro::Promise<T>;
template <typename T>
- using Future = boost::fibers::future<T>;
+ using Future = llcoro::Future<T>;
template <typename T>
static Future<T> getFuture(Promise<T>& promise) { return promise.get_future(); }
// use mutex, lock, condition_variable suitable for coroutines
- using Mutex = boost::fibers::mutex;
- using RMutex = boost::fibers::recursive_mutex;
- // With C++17, LockType is deprecated: at this point we can directly
- // declare 'std::unique_lock lk(some_mutex)' without explicitly stating
- // the mutex type. Sadly, making LockType an alias template for
- // std::unique_lock doesn't work the same way: Class Template Argument
- // Deduction only works for class templates, not alias templates.
- using LockType = std::unique_lock<Mutex>;
- using cv_status = boost::fibers::cv_status;
- using ConditionVariable = boost::fibers::condition_variable;
+ using Mutex = llcoro::Mutex;
+ using RMutex = llcoro::RMutex;
+ // LockType is deprecated; see llcoromutex.h
+ using LockType = llcoro::LockType;
+ using cv_status = llcoro::cv_status;
+ using ConditionVariable = llcoro::ConditionVariable;
/// for data local to each running coroutine
template <typename T>
@@ -329,6 +337,8 @@ private:
static CoroData& get_CoroData(const std::string& caller);
void saveException(const std::string& name, std::exception_ptr exc);
+ LLTempBoundListener mConn;
+
struct ExceptionData
{
ExceptionData(const std::string& nm, std::exception_ptr exc):
@@ -352,8 +362,10 @@ private:
// tweaked name of the current coroutine
const std::string mName;
- // set_consuming() state
- bool mConsuming;
+ // set_consuming() state -- don't consume events unless specifically directed
+ bool mConsuming{ false };
+ // killed by which coroutine
+ std::string mKilledBy;
// setStatus() state
std::string mStatus;
F64 mCreationTime; // since epoch
diff --git a/indra/llcommon/lldate.cpp b/indra/llcommon/lldate.cpp
index b38864688d..5f51f40232 100644
--- a/indra/llcommon/lldate.cpp
+++ b/indra/llcommon/lldate.cpp
@@ -41,7 +41,9 @@
#include "llstring.h"
#include "llfasttimer.h"
-static const F64 LL_APR_USEC_PER_SEC = 1000000.0;
+static const LLDate::timestamp DATE_EPOCH = 0.0;
+
+static const LLDate::timestamp LL_APR_USEC_PER_SEC = 1000000.0;
// should be APR_USEC_PER_SEC, but that relies on INT64_C which
// isn't defined in glib under our build set up for some reason
@@ -224,13 +226,13 @@ bool LLDate::fromStream(std::istream& s)
return false;
}
- F64 seconds_since_epoch = time / LL_APR_USEC_PER_SEC;
+ timestamp seconds_since_epoch = time / LL_APR_USEC_PER_SEC;
// check for fractional
c = s.peek();
if(c == '.')
{
- F64 fractional = 0.0;
+ timestamp fractional = 0.0;
s >> fractional;
seconds_since_epoch += fractional;
}
@@ -290,12 +292,12 @@ bool LLDate::fromYMDHMS(S32 year, S32 month, S32 day, S32 hour, S32 min, S32 sec
return true;
}
-F64 LLDate::secondsSinceEpoch() const
+LLDate::timestamp LLDate::secondsSinceEpoch() const
{
return mSecondsSinceEpoch;
}
-void LLDate::secondsSinceEpoch(F64 seconds)
+void LLDate::secondsSinceEpoch(timestamp seconds)
{
mSecondsSinceEpoch = seconds;
}
diff --git a/indra/llcommon/lldate.h b/indra/llcommon/lldate.h
index 1a69a04232..0afe0b0599 100644
--- a/indra/llcommon/lldate.h
+++ b/indra/llcommon/lldate.h
@@ -45,6 +45,8 @@ class LL_COMMON_API LLDate
{
static constexpr F64 DATE_EPOCH = 0.0;
public:
+ using timestamp = F64;
+
/**
* @brief Construct a date equal to epoch.
*/
@@ -100,14 +102,14 @@ public:
*
* @return The number of seconds since epoch UTC.
*/
- F64 secondsSinceEpoch() const;
+ timestamp secondsSinceEpoch() const;
/**
* @brief Set the date in seconds since epoch.
*
* @param seconds The number of seconds since epoch UTC.
*/
- void secondsSinceEpoch(F64 seconds);
+ void secondsSinceEpoch(timestamp seconds);
/**
* @brief Create an LLDate object set to the current time.
@@ -144,7 +146,7 @@ public:
private:
- F64 mSecondsSinceEpoch;
+ timestamp mSecondsSinceEpoch;
};
// Helper function to stream out a date
diff --git a/indra/llcommon/lldefs.h b/indra/llcommon/lldefs.h
index 2fbb26dc1a..d4b063f88c 100644
--- a/indra/llcommon/lldefs.h
+++ b/indra/llcommon/lldefs.h
@@ -28,6 +28,7 @@
#define LL_LLDEFS_H
#include "stdtypes.h"
+#include <cassert>
#include <type_traits>
// Often used array indices
@@ -169,6 +170,38 @@ constexpr U32 MAXADDRSTR = 17; // 123.567.901.345 = 15 chars + \0 +
// llclampb(a) // clamps a to [0 .. 255]
//
+// llless(d0, d1) safely compares d0 < d1 even if one is signed and the other
+// is unsigned. A simple (d0 < d1) expression converts the signed operand to
+// unsigned before comparing. If the signed operand is negative, that flips
+// the negative value to a huge positive value, producing the wrong answer!
+// llless() specifically addresses that case.
+template <typename T0, typename T1>
+inline bool llless(T0 d0, T1 d1)
+{
+ if constexpr (std::is_signed_v<T0> && ! std::is_signed_v<T1>)
+ {
+ // T0 signed, T1 unsigned: negative d0 is less than any unsigned d1
+ if (d0 < 0)
+ return true;
+ // both are non-negative: explicitly cast to avoid C4018
+ return std::make_unsigned_t<T0>(d0) < d1;
+ }
+ else if constexpr (! std::is_signed_v<T0> && std::is_signed_v<T1>)
+ {
+ // T0 unsigned, T1 signed: any unsigned d0 is greater than negative d1
+ if (d1 < 0)
+ return false;
+ // both are non-negative: explicitly cast to avoid C4018
+ return d0 < std::make_unsigned_t<T1>(d1);
+ }
+ else
+ {
+ // both T0 and T1 are signed, or both are unsigned:
+ // straightforward comparison works
+ return d0 < d1;
+ }
+}
+
// recursion tail
template <typename T>
inline auto llmax(T data)
@@ -180,7 +213,7 @@ template <typename T0, typename T1, typename... Ts>
inline auto llmax(T0 d0, T1 d1, Ts... rest)
{
auto maxrest = llmax(d1, rest...);
- return (d0 > maxrest)? d0 : maxrest;
+ return llless(maxrest, d0)? d0 : maxrest;
}
// recursion tail
@@ -194,12 +227,28 @@ template <typename T0, typename T1, typename... Ts>
inline auto llmin(T0 d0, T1 d1, Ts... rest)
{
auto minrest = llmin(d1, rest...);
- return (d0 < minrest) ? d0 : minrest;
+ return llless(d0, minrest) ? d0 : minrest;
}
template <typename A, typename MIN, typename MAX>
inline A llclamp(A a, MIN minval, MAX maxval)
{
+ // The only troublesome case is if A is unsigned and either minval or
+ // maxval is both signed and negative. Casting a negative number to
+ // unsigned flips it to a huge positive number, making this llclamp() call
+ // ineffective.
+ if constexpr (! std::is_signed_v<A>)
+ {
+ if constexpr (std::is_signed_v<MIN>)
+ {
+ assert(minval >= 0);
+ }
+ if constexpr (std::is_signed_v<MAX>)
+ {
+ assert(maxval >= 0);
+ }
+ }
+
A aminval{ static_cast<A>(minval) }, amaxval{ static_cast<A>(maxval) };
if ( a < aminval )
{
diff --git a/indra/llcommon/lldependencies.h b/indra/llcommon/lldependencies.h
index 47b6fedc7d..d19cdd1f25 100644
--- a/indra/llcommon/lldependencies.h
+++ b/indra/llcommon/lldependencies.h
@@ -30,6 +30,8 @@
#if ! defined(LL_LLDEPENDENCIES_H)
#define LL_LLDEPENDENCIES_H
+#include "linden_common.h"
+#include "llexception.h"
#include <string>
#include <vector>
#include <set>
@@ -40,7 +42,6 @@
#include <boost/range/iterator_range.hpp>
#include <boost/function.hpp>
#include <boost/bind.hpp>
-#include "llexception.h"
/*****************************************************************************
* Utilities
diff --git a/indra/llcommon/llerror.cpp b/indra/llcommon/llerror.cpp
index 90c6ba309b..90294acd55 100644
--- a/indra/llcommon/llerror.cpp
+++ b/indra/llcommon/llerror.cpp
@@ -64,6 +64,8 @@
#define BOOST_STACKTRACE_GNU_SOURCE_NOT_REQUIRED
#include <boost/stacktrace.hpp>
+#include LLCOROS_RMUTEX_HEADER
+
namespace {
#if LL_WINDOWS
void debugger_print(const std::string& s)
@@ -1431,6 +1433,7 @@ namespace LLError
if (site.mLevel == LEVEL_ERROR)
{
+ writeToRecorders(site, stringize(boost::stacktrace::stacktrace()));
g->mFatalMessage = message;
if (s->mCrashFunction)
{
diff --git a/indra/llcommon/llerrorlegacy.h b/indra/llcommon/llerrorlegacy.h
deleted file mode 100644
index 693e1501d5..0000000000
--- a/indra/llcommon/llerrorlegacy.h
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * @file llerrorlegacy.h
- * @date January 2007
- * @brief old things from the older error system
- *
- * $LicenseInfo:firstyear=2007&license=viewerlgpl$
- * Second Life Viewer Source Code
- * Copyright (C) 2010, Linden Research, Inc.
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation;
- * version 2.1 of the License only.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- *
- * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
- * $/LicenseInfo$
- */
-
-#ifndef LL_LLERRORLEGACY_H
-#define LL_LLERRORLEGACY_H
-
-
-#endif // LL_LLERRORLEGACY_H
diff --git a/indra/llcommon/lleventapi.cpp b/indra/llcommon/lleventapi.cpp
index 8b724256b8..4672371b4f 100644
--- a/indra/llcommon/lleventapi.cpp
+++ b/indra/llcommon/lleventapi.cpp
@@ -55,6 +55,26 @@ LLEventAPI::~LLEventAPI()
{
}
+bool LLEventAPI::process(const LLSD& event) const
+{
+ // LLDispatchListener is documented to let DispatchError propagate if the
+ // incoming request has no "reply" key. That may be fine for internal-only
+ // use, but LLEventAPI opens the door for external requests. It should NOT
+ // be possible for any external requester to crash the viewer with an
+ // unhandled exception, especially not by something as simple as omitting
+ // the "reply" key.
+ try
+ {
+ return LLDispatchListener::process(event);
+ }
+ catch (const std::exception& err)
+ {
+ // log the exception, but otherwise ignore it
+ LL_WARNS("LLEventAPI") << LLError::Log::classname(err) << ": " << err.what() << LL_ENDL;
+ return false;
+ }
+}
+
LLEventAPI::Response::Response(const LLSD& seed, const LLSD& request, const LLSD::String& replyKey):
mResp(seed),
mReq(request),
diff --git a/indra/llcommon/lleventapi.h b/indra/llcommon/lleventapi.h
index 3ae820db51..da7c58e6f0 100644
--- a/indra/llcommon/lleventapi.h
+++ b/indra/llcommon/lleventapi.h
@@ -157,6 +157,7 @@ protected:
LLEventAPI(const LL::LazyEventAPIParams&);
private:
+ bool process(const LLSD& event) const override;
std::string mDesc;
};
diff --git a/indra/llcommon/lleventcoro.cpp b/indra/llcommon/lleventcoro.cpp
index e1fc4764f6..33db27a116 100644
--- a/indra/llcommon/lleventcoro.cpp
+++ b/indra/llcommon/lleventcoro.cpp
@@ -119,7 +119,7 @@ void llcoro::suspendUntilTimeout(float seconds)
// We used to call boost::this_fiber::sleep_for(). But some coroutines
// (e.g. LLExperienceCache::idleCoro()) sit in a suspendUntilTimeout()
// loop, in which case a sleep_for() call risks sleeping through shutdown.
- // So instead, listen for "LLApp" state-changing events -- which
+ // So instead, listen for LLApp state-changing events -- which
// fortunately is handled for us by suspendUntilEventOnWithTimeout().
// Wait for an event on a bogus LLEventPump on which nobody ever posts
// events. Don't make it static because that would force instantiation of
@@ -132,8 +132,8 @@ void llcoro::suspendUntilTimeout(float seconds)
// Timeout is the NORMAL case for this call!
static LLSD timedout;
// Deliver, but ignore, timedout when (as usual) we did not receive any
- // "LLApp" event. The point is that suspendUntilEventOnWithTimeout() will
- // itself throw Stopping when "LLApp" starts broadcasting shutdown events.
+ // LLApp event. The point is that suspendUntilEventOnWithTimeout() will
+ // itself throw Stopping when LLApp starts broadcasting shutdown events.
suspendUntilEventOnWithTimeout(bogus, seconds, timedout);
}
@@ -167,33 +167,26 @@ postAndSuspendSetup(const std::string& callerName,
// "LLApp" were an LLEventMailDrop. But if we ever go there, we'd want to
// notice the pending LLApp status first.
LLBoundListener stopper(
- LLEventPumps::instance().obtain("LLApp").listen(
+ LLCoros::getStopListener(
listenerName,
+ LLCoros::instance().getName(),
[&promise, listenerName](const LLSD& status)
{
- // anything except "running" should wake up the waiting
- // coroutine
- auto& statsd = status["status"];
- if (statsd.asString() != "running")
+ LL_DEBUGS("lleventcoro") << listenerName
+ << " spotted status " << status
+ << ", throwing Stopping" << LL_ENDL;
+ try
+ {
+ promise.set_exception(
+ std::make_exception_ptr(
+ LLCoros::Stopping("status " + stringize(status))));
+ }
+ catch (const boost::fibers::promise_already_satisfied&)
{
- LL_DEBUGS("lleventcoro") << listenerName
- << " spotted status " << statsd
- << ", throwing Stopping" << LL_ENDL;
- try
- {
- promise.set_exception(
- std::make_exception_ptr(
- LLCoros::Stopping("status " + statsd.asString())));
- }
- catch (const boost::fibers::promise_already_satisfied&)
- {
- LL_WARNS("lleventcoro") << listenerName
- << " couldn't throw Stopping "
- "because promise already set" << LL_ENDL;
- }
+ LL_WARNS("lleventcoro") << listenerName
+ << " couldn't throw Stopping "
+ "because promise already set" << LL_ENDL;
}
- // do not consume -- every listener must see status
- return false;
}));
LLBoundListener connection(
replyPump.listen(
diff --git a/indra/llcommon/lleventdispatcher.cpp b/indra/llcommon/lleventdispatcher.cpp
index 9dd6864cff..12b966fadf 100644
--- a/indra/llcommon/lleventdispatcher.cpp
+++ b/indra/llcommon/lleventdispatcher.cpp
@@ -800,7 +800,7 @@ void LLDispatchListener::call_one(const LLSD& name, const LLSD& event) const
{
result = (*this)(event);
}
- catch (const DispatchError& err)
+ catch (const std::exception& err)
{
if (! event.has(mReplyKey))
{
@@ -810,7 +810,10 @@ void LLDispatchListener::call_one(const LLSD& name, const LLSD& event) const
// Here there was an error and the incoming event has mReplyKey. Reply
// with a map containing an "error" key explaining the problem.
- return reply(llsd::map("error", err.what()), event);
+ return reply(llsd::map("error",
+ stringize(LLError::Log::classname(err),
+ ": ", err.what())),
+ event);
}
// We seem to have gotten a valid result. But we don't know whether the
@@ -846,7 +849,7 @@ void LLDispatchListener::call_map(const LLSD& reqmap, const LLSD& event) const
{
// in case of errors, tell user the dispatch key, the fact that
// we're processing a request map and the current key in that map
- SetState(this, '[', key, '[', name, "]]");
+ SetState transient(this, '[', key, '[', name, "]]");
// With this form, capture return value even if undefined:
// presence of the key in the response map can be used to detect
// which request keys succeeded.
@@ -869,7 +872,7 @@ void LLDispatchListener::call_map(const LLSD& reqmap, const LLSD& event) const
if (! event.has(mReplyKey))
{
// can't send reply, throw
- sCallFail<DispatchError>(error);
+ callFail<DispatchError>(error);
}
else
{
@@ -923,7 +926,7 @@ void LLDispatchListener::call_array(const LLSD& reqarray, const LLSD& event) con
// in case of errors, tell user the dispatch key, the fact that
// we're processing a request array, the current entry in that
// array and the corresponding callable name
- SetState(this, '[', key, '[', i, "]=", name, ']');
+ SetState transient(this, '[', key, '[', i, "]=", name, ']');
// With this form, capture return value even if undefined
results.append((*this)(name, args));
}
@@ -944,7 +947,7 @@ void LLDispatchListener::call_array(const LLSD& reqarray, const LLSD& event) con
if (! event.has(mReplyKey))
{
// can't send reply, throw
- sCallFail<DispatchError>(error);
+ callFail<DispatchError>(error);
}
else
{
diff --git a/indra/llcommon/lleventdispatcher.h b/indra/llcommon/lleventdispatcher.h
index 4c3c0f3414..6b5524e1eb 100644
--- a/indra/llcommon/lleventdispatcher.h
+++ b/indra/llcommon/lleventdispatcher.h
@@ -35,7 +35,6 @@
#include <boost/fiber/fss.hpp>
#include <boost/function_types/is_member_function_pointer.hpp>
#include <boost/function_types/is_nonmember_callable_builtin.hpp>
-#include <boost/hof/is_invocable.hpp> // until C++17, when we get std::is_invocable
#include <boost/iterator/transform_iterator.hpp>
#include <functional> // std::function
#include <memory> // std::unique_ptr
@@ -48,6 +47,7 @@
#include "llevents.h"
#include "llptrto.h"
#include "llsdutil.h"
+#include "stringize.h"
class LLSD;
@@ -99,7 +99,7 @@ public:
template <typename CALLABLE,
typename=typename std::enable_if<
- boost::hof::is_invocable<CALLABLE, LLSD>::value
+ std::is_invocable<CALLABLE, LLSD>::value
>::type>
void add(const std::string& name,
const std::string& desc,
@@ -296,7 +296,7 @@ public:
*/
template <typename CALLABLE,
typename=typename std::enable_if<
- ! boost::hof::is_invocable<CALLABLE, LLSD>()
+ ! std::is_invocable<CALLABLE, LLSD>()
>::type>
void add(const std::string& name,
const std::string& desc,
@@ -338,7 +338,7 @@ public:
template<typename Function,
typename = typename std::enable_if<
boost::function_types::is_nonmember_callable_builtin<Function>::value &&
- ! boost::hof::is_invocable<Function, LLSD>::value
+ ! std::is_invocable<Function, LLSD>::value
>::type>
void add(const std::string& name, const std::string& desc, Function f,
const LLSD& params, const LLSD& defaults=LLSD());
@@ -851,8 +851,12 @@ public:
ARGS&&... args);
virtual ~LLDispatchListener() {}
+ std::string getPumpName() const { return getName(); }
+
+protected:
+ virtual bool process(const LLSD& event) const;
+
private:
- bool process(const LLSD& event) const;
void call_one(const LLSD& name, const LLSD& event) const;
void call_map(const LLSD& reqmap, const LLSD& event) const;
void call_array(const LLSD& reqarray, const LLSD& event) const;
diff --git a/indra/llcommon/lleventfilter.cpp b/indra/llcommon/lleventfilter.cpp
index 604ee8a42d..2b5401e9f7 100644
--- a/indra/llcommon/lleventfilter.cpp
+++ b/indra/llcommon/lleventfilter.cpp
@@ -33,20 +33,19 @@
// STL headers
// std headers
// external library headers
-#include <boost/bind.hpp>
// other Linden headers
+#include "lldate.h"
#include "llerror.h" // LL_ERRS
+#include "lleventtimer.h"
#include "llsdutil.h" // llsd_matches()
#include "stringize.h"
-#include "lleventtimer.h"
-#include "lldate.h"
/*****************************************************************************
* LLEventFilter
*****************************************************************************/
LLEventFilter::LLEventFilter(LLEventPump& source, const std::string& name, bool tweak):
LLEventStream(name, tweak),
- mSource(source.listen(getName(), boost::bind(&LLEventFilter::post, this, _1)))
+ mSource(source.listen(getName(), [this](const LLSD& event){ return post(event); }))
{
}
@@ -74,136 +73,52 @@ bool LLEventMatching::post(const LLSD& event)
}
/*****************************************************************************
-* LLEventTimeoutBase
+* LLEventTimeout
*****************************************************************************/
-LLEventTimeoutBase::LLEventTimeoutBase():
+LLEventTimeout::LLEventTimeout():
LLEventFilter("timeout")
{
}
-LLEventTimeoutBase::LLEventTimeoutBase(LLEventPump& source):
+LLEventTimeout::LLEventTimeout(LLEventPump& source):
LLEventFilter(source, "timeout")
{
}
-void LLEventTimeoutBase::actionAfter(F32 seconds, const Action& action)
+void LLEventTimeout::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));
- }
+ mTimer = LL::Timers::instance().scheduleAfter(action, seconds);
}
-class ErrorAfter
+void LLEventTimeout::errorAfter(F32 seconds, const std::string& message)
{
-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));
+ actionAfter(
+ seconds,
+ [message=message]
+ {
+ LL_ERRS("LLEventTimeout") << message << LL_ENDL;
+ });
}
-class EventAfter
+void LLEventTimeout::eventAfter(F32 seconds, const LLSD& event)
{
-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));
+ actionAfter(seconds, [this, event]{ post(event); });
}
-bool LLEventTimeoutBase::post(const LLSD& event)
+bool LLEventTimeout::post(const LLSD& event)
{
cancel();
return LLEventStream::post(event);
}
-void LLEventTimeoutBase::cancel()
+void LLEventTimeout::cancel()
{
- mMainloop.disconnect();
+ mTimer.cancel();
}
-bool LLEventTimeoutBase::tick(const LLSD&)
+bool LLEventTimeout::running() const
{
- if (countdownElapsed())
- {
- cancel();
- mAction();
- }
- return false; // show event to other listeners
-}
-
-bool LLEventTimeoutBase::running() const
-{
- return mMainloop.connected();
-}
-
-/*****************************************************************************
-* LLEventTimeout
-*****************************************************************************/
-LLEventTimeout::LLEventTimeout() {}
-
-LLEventTimeout::LLEventTimeout(LLEventPump& source):
- LLEventTimeoutBase(source)
-{
-}
-
-void LLEventTimeout::setCountdown(F32 seconds)
-{
- mTimer.setTimerExpirySec(seconds);
-}
-
-bool LLEventTimeout::countdownElapsed() const
-{
- return mTimer.hasExpired();
-}
-
-LLEventTimer* LLEventTimeout::post_every(F32 period, const std::string& pump, const LLSD& data)
-{
- return LLEventTimer::run_every(
- period,
- [pump, data](){ LLEventPumps::instance().obtain(pump).post(data); });
-}
-
-LLEventTimer* LLEventTimeout::post_at(const LLDate& time, const std::string& pump, const LLSD& data)
-{
- return LLEventTimer::run_at(
- time,
- [pump, data](){ LLEventPumps::instance().obtain(pump).post(data); });
-}
-
-LLEventTimer* LLEventTimeout::post_after(F32 interval, const std::string& pump, const LLSD& data)
-{
- return LLEventTimer::run_after(
- interval,
- [pump, data](){ LLEventPumps::instance().obtain(pump).post(data); });
+ return LL::Timers::instance().isRunning(mTimer);
}
/*****************************************************************************
@@ -246,21 +161,21 @@ void LLEventBatch::setSize(std::size_t size)
}
/*****************************************************************************
-* LLEventThrottleBase
+* LLEventThrottle
*****************************************************************************/
-LLEventThrottleBase::LLEventThrottleBase(F32 interval):
+LLEventThrottle::LLEventThrottle(F32 interval):
LLEventFilter("throttle"),
mInterval(interval),
mPosts(0)
{}
-LLEventThrottleBase::LLEventThrottleBase(LLEventPump& source, F32 interval):
+LLEventThrottle::LLEventThrottle(LLEventPump& source, F32 interval):
LLEventFilter(source, "throttle"),
mInterval(interval),
mPosts(0)
{}
-void LLEventThrottleBase::flush()
+void LLEventThrottle::flush()
{
// flush() is a no-op unless there's something pending.
// Don't test mPending because there's no requirement that the consumer
@@ -281,12 +196,12 @@ void LLEventThrottleBase::flush()
}
}
-LLSD LLEventThrottleBase::pending() const
+LLSD LLEventThrottle::pending() const
{
return mPending;
}
-bool LLEventThrottleBase::post(const LLSD& event)
+bool LLEventThrottle::post(const LLSD& event)
{
// Always capture most recent post() event data. If caller wants to
// aggregate multiple events, let them retrieve pending() and modify
@@ -311,13 +226,13 @@ bool LLEventThrottleBase::post(const LLSD& event)
// timeRemaining tells us how much longer it will be until
// mInterval seconds since the last flush() call. At that time,
// flush() deferred events.
- alarmActionAfter(timeRemaining, boost::bind(&LLEventThrottleBase::flush, this));
+ alarmActionAfter(timeRemaining, [this]{ flush(); });
}
}
return false;
}
-void LLEventThrottleBase::setInterval(F32 interval)
+void LLEventThrottle::setInterval(F32 interval)
{
F32 oldInterval = mInterval;
mInterval = interval;
@@ -349,41 +264,30 @@ void LLEventThrottleBase::setInterval(F32 interval)
// and if mAlarm is running, reset that too
if (alarmRunning())
{
- alarmActionAfter(timeRemaining, boost::bind(&LLEventThrottleBase::flush, this));
+ alarmActionAfter(timeRemaining, [this](){ flush(); });
}
}
}
}
-F32 LLEventThrottleBase::getDelay() const
+F32 LLEventThrottle::getDelay() const
{
return timerGetRemaining();
}
-/*****************************************************************************
-* LLEventThrottle implementation
-*****************************************************************************/
-LLEventThrottle::LLEventThrottle(F32 interval):
- LLEventThrottleBase(interval)
-{}
-
-LLEventThrottle::LLEventThrottle(LLEventPump& source, F32 interval):
- LLEventThrottleBase(source, interval)
-{}
-
-void LLEventThrottle::alarmActionAfter(F32 interval, const LLEventTimeoutBase::Action& action)
+void LLEventThrottle::alarmActionAfter(F32 interval, const LLEventTimeout::Action& action)
{
- mAlarm.actionAfter(interval, action);
+ mAlarm = LL::Timers::instance().scheduleAfter(action, interval);
}
bool LLEventThrottle::alarmRunning() const
{
- return mAlarm.running();
+ return LL::Timers::instance().isRunning(mAlarm);
}
void LLEventThrottle::alarmCancel()
{
- return mAlarm.cancel();
+ LL::Timers::instance().cancel(mAlarm);
}
void LLEventThrottle::timerSet(F32 interval)
@@ -461,21 +365,22 @@ bool LLEventLogProxy::post(const LLSD& event) /* override */
}
LLBoundListener LLEventLogProxy::listen_impl(const std::string& name,
- const LLEventListener& target,
+ const LLAwareListener& target,
const NameList& after,
const NameList& before)
{
LL_DEBUGS("LogProxy") << "LLEventLogProxy('" << getName() << "').listen('"
<< name << "')" << LL_ENDL;
return mPump.listen(name,
- [this, name, target](const LLSD& event)->bool
- { return listener(name, target, event); },
+ [this, name, target](const LLBoundListener& conn, const LLSD& event)
+ { return listener(conn, name, target, event); },
after,
before);
}
-bool LLEventLogProxy::listener(const std::string& name,
- const LLEventListener& target,
+bool LLEventLogProxy::listener(const LLBoundListener& conn,
+ const std::string& name,
+ const LLAwareListener& target,
const LLSD& event) const
{
auto eventminus = event;
@@ -487,7 +392,7 @@ bool LLEventLogProxy::listener(const std::string& name,
}
std::string hdr{STRINGIZE(getName() << " to " << name << " " << counter)};
LL_INFOS("LogProxy") << hdr << ": " << eventminus << LL_ENDL;
- bool result = target(eventminus);
+ bool result = target(conn, eventminus);
LL_INFOS("LogProxy") << hdr << " => " << result << LL_ENDL;
return result;
}
diff --git a/indra/llcommon/lleventfilter.h b/indra/llcommon/lleventfilter.h
index d8c7e15a27..09ef81a6f5 100644
--- a/indra/llcommon/lleventfilter.h
+++ b/indra/llcommon/lleventfilter.h
@@ -29,13 +29,13 @@
#if ! defined(LL_LLEVENTFILTER_H)
#define LL_LLEVENTFILTER_H
+#include "llcallbacklist.h"
#include "llevents.h"
-#include "stdtypes.h"
-#include "lltimer.h"
#include "llsdutil.h"
-#include <boost/function.hpp>
+#include "lltimer.h"
+#include "stdtypes.h"
+#include <functional>
-class LLEventTimer;
class LLDate;
/**
@@ -78,22 +78,27 @@ private:
/**
* 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.
+ * time, take a specified action.
*
- * @NOTE This is an abstract base class so that, for testing, we can use an
- * alternate "timer" that doesn't actually consume real time.
+ * @NOTE: Caution should be taken when using the LLEventTimeout(LLEventPump &)
+ * constructor to ensure that the upstream event pump is not an LLEventMaildrop
+ * or any other kind of store and forward pump which may have events outstanding.
+ * Using this constructor will cause the upstream event pump to fire any pending
+ * events and could result in the invocation of a virtual method before the timeout
+ * has been fully constructed. The timeout should instead be constructed separately
+ * from the event pump and attached using the listen method.
+ * See llcoro::suspendUntilEventOnWithTimeout() for an example.
*/
-class LL_COMMON_API LLEventTimeoutBase: public LLEventFilter
+class LL_COMMON_API LLEventTimeout: public LLEventFilter
{
public:
/// construct standalone
- LLEventTimeoutBase();
+ LLEventTimeout();
/// construct and connect
- LLEventTimeoutBase(LLEventPump& source);
+ LLEventTimeout(LLEventPump& source);
/// Callable, can be constructed with boost::bind()
- typedef boost::function<void()> Action;
+ typedef std::function<void()> Action;
/**
* Start countdown timer for the specified number of @a seconds. Forward
@@ -120,8 +125,8 @@ public:
* @endcode
*
* @NOTE
- * The implementation relies on frequent events on the LLEventPump named
- * "mainloop".
+ * The implementation relies on frequent calls to
+ * gIdleCallbacks.callFunctions().
*/
void actionAfter(F32 seconds, const Action& action);
@@ -134,7 +139,7 @@ public:
* 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).
+ * gIdleCallbacks.callFunctions() 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
@@ -184,55 +189,9 @@ public:
/// Is this timer currently running?
bool running() const;
-protected:
- virtual void setCountdown(F32 seconds) = 0;
- virtual bool countdownElapsed() const = 0;
-
private:
- bool tick(const LLSD&);
-
- LLTempBoundListener mMainloop;
- Action mAction;
-};
-
-/**
- * Production implementation of LLEventTimoutBase.
- *
- * @NOTE: Caution should be taken when using the LLEventTimeout(LLEventPump &)
- * constructor to ensure that the upstream event pump is not an LLEventMaildrop
- * or any other kind of store and forward pump which may have events outstanding.
- * Using this constructor will cause the upstream event pump to fire any pending
- * events and could result in the invocation of a virtual method before the timeout
- * has been fully constructed. The timeout should instead be connected upstream
- * from the event pump and attached using the listen method.
- * See llcoro::suspendUntilEventOnWithTimeout() for an example.
- */
-
-class LL_COMMON_API LLEventTimeout: public LLEventTimeoutBase
-{
-public:
- LLEventTimeout();
- LLEventTimeout(LLEventPump& source);
-
- /// using LLEventTimeout as namespace for free functions
- /// Post event to specified LLEventPump every period seconds. Delete
- /// returned LLEventTimer* to cancel.
- static LLEventTimer* post_every(F32 period, const std::string& pump, const LLSD& data);
- /// Post event to specified LLEventPump at specified future time. Call
- /// LLEventTimer::getInstance(returned pointer) to check whether it's still
- /// pending; if so, delete the pointer to cancel.
- static LLEventTimer* post_at(const LLDate& time, const std::string& pump, const LLSD& data);
- /// Post event to specified LLEventPump after specified interval. Call
- /// LLEventTimer::getInstance(returned pointer) to check whether it's still
- /// pending; if so, delete the pointer to cancel.
- static LLEventTimer* post_after(F32 interval, const std::string& pump, const LLSD& data);
-
-protected:
- virtual void setCountdown(F32 seconds);
- virtual bool countdownElapsed() const;
-
-private:
- LLTimer mTimer;
+ // Use a temp_handle_t so it's canceled on destruction.
+ LL::Timers::temp_handle_t mTimer;
};
/**
@@ -264,7 +223,7 @@ private:
};
/**
- * LLEventThrottleBase: construct with a time interval. Regardless of how
+ * LLEventThrottle: construct with a time interval. Regardless of how
* frequently you call post(), LLEventThrottle will pass on an event to
* its listeners no more often than once per specified interval.
*
@@ -297,13 +256,13 @@ private:
* alternate "timer" that doesn't actually consume real time. See
* LLEventThrottle.
*/
-class LL_COMMON_API LLEventThrottleBase: public LLEventFilter
+class LL_COMMON_API LLEventThrottle: public LLEventFilter
{
public:
// pass time interval
- LLEventThrottleBase(F32 interval);
+ LLEventThrottle(F32 interval);
// construct and connect
- LLEventThrottleBase(LLEventPump& source, F32 interval);
+ LLEventThrottle(LLEventPump& source, F32 interval);
// force out any deferred events
void flush();
@@ -324,45 +283,24 @@ public:
// time until next event would be passed through, 0.0 if now
F32 getDelay() const;
-protected:
- // Implement these time-related methods for a valid LLEventThrottleBase
- // subclass (see LLEventThrottle). For testing, we use a subclass that
- // doesn't involve actual elapsed time.
- virtual void alarmActionAfter(F32 interval, const LLEventTimeoutBase::Action& action) = 0;
- virtual bool alarmRunning() const = 0;
- virtual void alarmCancel() = 0;
- virtual void timerSet(F32 interval) = 0;
- virtual F32 timerGetRemaining() const = 0;
-
private:
- // remember throttle interval
- F32 mInterval;
- // count post() calls since last flush()
- std::size_t mPosts;
+ void alarmActionAfter(F32 interval, const LLEventTimeout::Action& action);
+ bool alarmRunning() const;
+ void alarmCancel();
+ void timerSet(F32 interval);
+ F32 timerGetRemaining() const;
+
// pending event data from most recent deferred event
LLSD mPending;
-};
-
-/**
- * Production implementation of LLEventThrottle.
- */
-class LLEventThrottle: public LLEventThrottleBase
-{
-public:
- LLEventThrottle(F32 interval);
- LLEventThrottle(LLEventPump& source, F32 interval);
-
-private:
- virtual void alarmActionAfter(F32 interval, const LLEventTimeoutBase::Action& action) /*override*/;
- virtual bool alarmRunning() const /*override*/;
- virtual void alarmCancel() /*override*/;
- virtual void timerSet(F32 interval) /*override*/;
- virtual F32 timerGetRemaining() const /*override*/;
-
- // use this to arrange a deferred flush() call
- LLEventTimeout mAlarm;
// use this to track whether we're within mInterval of last flush()
LLTimer mTimer;
+ // count post() calls since last flush()
+ std::size_t mPosts;
+ // remember throttle interval
+ F32 mInterval;
+
+ // use this to arrange a deferred flush() call
+ LL::Timers::handle_t mAlarm;
};
/**
@@ -521,7 +459,7 @@ public:
LLEventLogProxy(LLEventPump& source, const std::string& name, bool tweak=false);
/// register a new listener
- LLBoundListener listen_impl(const std::string& name, const LLEventListener& target,
+ LLBoundListener listen_impl(const std::string& name, const LLAwareListener& target,
const NameList& after, const NameList& before);
/// Post an event to all listeners
@@ -531,8 +469,9 @@ private:
/// This method intercepts each call to any target listener. We pass it
/// the listener name and the caller's intended target listener plus the
/// posted LLSD event.
- bool listener(const std::string& name,
- const LLEventListener& target,
+ bool listener(const LLBoundListener& conn,
+ const std::string& name,
+ const LLAwareListener& target,
const LLSD& event) const;
LLEventPump& mPump;
diff --git a/indra/llcommon/llevents.cpp b/indra/llcommon/llevents.cpp
index 3c6743eac9..6531b951b9 100644
--- a/indra/llcommon/llevents.cpp
+++ b/indra/llcommon/llevents.cpp
@@ -43,6 +43,7 @@
#include <typeinfo>
#include <cmath>
#include <cctype>
+#include <iomanip> // std::quoted
// external library headers
#include <boost/range/iterator_range.hpp>
#if LL_WINDOWS
@@ -54,10 +55,11 @@
#pragma warning (pop)
#endif
// other Linden headers
-#include "stringize.h"
#include "llerror.h"
-#include "llsdutil.h"
+#include "lleventfilter.h"
#include "llexception.h"
+#include "llsdutil.h"
+#include "stringize.h"
#if LL_MSVC
#pragma warning (disable : 4702)
#endif
@@ -71,7 +73,9 @@ LLEventPumps::LLEventPumps():
{ "LLEventStream", [](const std::string& name, bool tweak, const std::string& /*type*/)
{ return new LLEventStream(name, tweak); } },
{ "LLEventMailDrop", [](const std::string& name, bool tweak, const std::string& /*type*/)
- { return new LLEventMailDrop(name, tweak); } }
+ { return new LLEventMailDrop(name, tweak); } },
+ { "LLEventLogProxy", [](const std::string& name, bool tweak, const std::string& /*type*/)
+ { return new LLEventLogProxyFor<LLEventStream>(name, tweak); } }
},
mTypes
{
@@ -186,8 +190,13 @@ bool LLEventPumps::post(const std::string&name, const LLSD&message)
PumpMap::iterator found = mPumpMap.find(name);
if (found == mPumpMap.end())
+ {
+ LL_DEBUGS("LLEventPumps") << "LLEventPump(" << std::quoted(name) << ") not found"
+ << LL_ENDL;
return false;
+ }
+// LL_DEBUGS("LLEventPumps") << "posting to " << name << ": " << message << LL_ENDL;
return (*found).second->post(message);
}
@@ -350,8 +359,7 @@ const std::string LLEventPump::ANONYMOUS = std::string();
LLEventPump::LLEventPump(const std::string& name, bool tweak):
// Register every new instance with LLEventPumps
- mRegistry(LLEventPumps::instance().getHandle()),
- mName(mRegistry.get()->registerNew(*this, name, tweak)),
+ mName(LLEventPumps::instance().registerNew(*this, name, tweak)),
mSignal(std::make_shared<LLStandardSignal>()),
mEnabled(true)
{}
@@ -364,10 +372,9 @@ LLEventPump::~LLEventPump()
{
// Unregister this doomed instance from LLEventPumps -- but only if
// LLEventPumps is still around!
- LLEventPumps* registry = mRegistry.get();
- if (registry)
+ if (LLEventPumps::instanceExists())
{
- registry->unregister(*this);
+ LLEventPumps::instance().unregister(*this);
}
}
@@ -382,7 +389,7 @@ std::string LLEventPump::inventName(const std::string& pfx)
void LLEventPump::clear()
{
- LLCoros::LockType lock(mConnectionListMutex);
+ llcoro::LockType lock(mConnectionListMutex);
// Destroy the original LLStandardSignal instance, replacing it with a
// whole new one.
mSignal = std::make_shared<LLStandardSignal>();
@@ -394,7 +401,7 @@ void LLEventPump::reset()
{
// Resetting mSignal is supposed to disconnect everything on its own
// But due to crash on 'reset' added explicit cleanup to get more data
- LLCoros::LockType lock(mConnectionListMutex);
+ llcoro::LockType lock(mConnectionListMutex);
ConnectionMap::const_iterator iter = mConnections.begin();
ConnectionMap::const_iterator end = mConnections.end();
while (iter!=end)
@@ -408,18 +415,18 @@ void LLEventPump::reset()
//mDeps.clear();
}
-LLBoundListener LLEventPump::listen_impl(const std::string& name, const LLEventListener& listener,
+LLBoundListener LLEventPump::listen_impl(const std::string& name, const LLAwareListener& listener,
const NameList& after,
const NameList& before)
{
if (!mSignal)
{
- LL_WARNS() << "Can't connect listener" << LL_ENDL;
+ LL_WARNS("LLEventPump") << "Can't connect listener" << LL_ENDL;
// connect will fail, return dummy
return LLBoundListener();
}
- LLCoros::LockType lock(mConnectionListMutex);
+ llcoro::LockType lock(mConnectionListMutex);
float nodePosition = 1.0;
@@ -568,7 +575,7 @@ LLBoundListener LLEventPump::listen_impl(const std::string& name, const LLEventL
}
// Now that newNode has a value that places it appropriately in mSignal,
// connect it.
- LLBoundListener bound = mSignal->connect(nodePosition, listener);
+ LLBoundListener bound = mSignal->connect_extended(nodePosition, listener);
if (!name.empty())
{ // note that we are not tracking anonymous listeners here either.
@@ -582,7 +589,7 @@ LLBoundListener LLEventPump::listen_impl(const std::string& name, const LLEventL
LLBoundListener LLEventPump::getListener(const std::string& name)
{
- LLCoros::LockType lock(mConnectionListMutex);
+ llcoro::LockType lock(mConnectionListMutex);
ConnectionMap::const_iterator found = mConnections.find(name);
if (found != mConnections.end())
{
@@ -594,7 +601,7 @@ LLBoundListener LLEventPump::getListener(const std::string& name)
void LLEventPump::stopListening(const std::string& name)
{
- LLCoros::LockType lock(mConnectionListMutex);
+ llcoro::LockType lock(mConnectionListMutex);
ConnectionMap::iterator found = mConnections.find(name);
if (found != mConnections.end())
{
@@ -652,7 +659,7 @@ bool LLEventMailDrop::post(const LLSD& event)
}
LLBoundListener LLEventMailDrop::listen_impl(const std::string& name,
- const LLEventListener& listener,
+ const LLAwareListener& listener,
const NameList& after,
const NameList& before)
{
@@ -661,7 +668,10 @@ LLBoundListener LLEventMailDrop::listen_impl(const std::string& name,
// Remove any that this listener consumes -- Effective STL, Item 9.
for (auto hi(mEventHistory.begin()), hend(mEventHistory.end()); hi != hend; )
{
- if (listener(*hi))
+ // We don't actually have an LLBoundListener in hand, and we won't
+ // until the base-class listen_impl() call below. Pass an empty
+ // instance.
+ if (listener({}, *hi))
{
// new listener consumed this event, erase it
hi = mEventHistory.erase(hi);
@@ -733,7 +743,7 @@ void LLReqID::stamp(LLSD& response) const
response["reqid"] = mReqid;
}
-bool sendReply(const LLSD& reply, const LLSD& request, const std::string& replyKey)
+bool sendReply(LLSD reply, const LLSD& request, const std::string& replyKey)
{
// If the original request has no value for replyKey, it's pointless to
// construct or send a reply event: on which LLEventPump should we send
@@ -746,13 +756,13 @@ bool sendReply(const LLSD& reply, const LLSD& request, const std::string& replyK
// Here the request definitely contains replyKey; reasonable to proceed.
- // Copy 'reply' to modify it.
- LLSD newreply(reply);
// Get the ["reqid"] element from request
LLReqID reqID(request);
- // and copy it to 'newreply'.
- reqID.stamp(newreply);
- // Send reply on LLEventPump named in request[replyKey]. Don't forget to
- // send the modified 'newreply' instead of the original 'reply'.
- return LLEventPumps::instance().obtain(request[replyKey]).post(newreply);
+ // and copy it to 'reply'.
+ reqID.stamp(reply);
+ // Send reply on LLEventPump named in request[replyKey] -- if that
+ // LLEventPump exists. If it does not, don't create it.
+ // This addresses the case in which a requester goes away before a
+ // particular LLEventAPI responds.
+ return LLEventPumps::instance().post(request[replyKey], reply);
}
diff --git a/indra/llcommon/llevents.h b/indra/llcommon/llevents.h
index 4bf1fa07a2..d339a09117 100644
--- a/indra/llcommon/llevents.h
+++ b/indra/llcommon/llevents.h
@@ -32,35 +32,21 @@
#if ! defined(LL_LLEVENTS_H)
#define LL_LLEVENTS_H
-#include <string>
+#include <deque>
+#include <functional>
#include <map>
#include <set>
+#include <string>
+#include <type_traits>
#include <vector>
-#include <deque>
-#include <functional>
#include <boost/signals2.hpp>
-#include <boost/bind.hpp>
-#include <boost/utility.hpp> // noncopyable
#include <boost/optional/optional.hpp>
-#include <boost/visit_each.hpp>
-#include <boost/ref.hpp> // reference_wrapper
-#include <boost/type_traits/is_pointer.hpp>
-#include <boost/static_assert.hpp>
-#include "llsd.h"
-#include "llsingleton.h"
+#include "llcoromutex.h"
#include "lldependencies.h"
-#include "llstl.h"
#include "llexception.h"
-#include "llhandle.h"
-#include "llcoros.h"
-
-/*==========================================================================*|
-// override this to allow binding free functions with more parameters
-#ifndef LLEVENTS_LISTENER_ARITY
-#define LLEVENTS_LISTENER_ARITY 10
-#endif
-|*==========================================================================*/
+#include "llsd.h"
+#include "llsingleton.h"
// hack for testing
#ifndef testable
@@ -144,6 +130,12 @@ typedef boost::signals2::signal<bool(const LLSD&), LLStopWhenHandled, float> LL
/// Methods that forward listeners (e.g. constructed with
/// <tt>boost::bind()</tt>) should accept (const LLEventListener&)
typedef LLStandardSignal::slot_type LLEventListener;
+/// Support a listener accepting (const LLBoundListener&, const LLSD&).
+/// Note that LLBoundListener::disconnect() is a const method: this feature is
+/// specifically intended to allow a listener to disconnect itself when done.
+typedef LLStandardSignal::extended_slot_type LLAwareListener;
+/// Accept a void listener too
+typedef std::function<void(const LLSD&)> LLVoidListener;
/// Result of registering a listener, supports <tt>connected()</tt>,
/// <tt>disconnect()</tt> and <tt>blocked()</tt>
typedef boost::signals2::connection LLBoundListener;
@@ -218,15 +210,7 @@ class LLEventPump;
* LLEventPumps is a Singleton manager through which one typically accesses
* this subsystem.
*/
-// LLEventPumps isa LLHandleProvider only for (hopefully rare) long-lived
-// class objects that must refer to this class late in their lifespan, say in
-// the destructor. Specifically, the case that matters is a possible reference
-// after LLEventPumps::deleteSingleton(). (Lingering LLEventPump instances are
-// capable of this.) In that case, instead of calling LLEventPumps::instance()
-// again -- resurrecting the deleted LLSingleton -- store an
-// LLHandle<LLEventPumps> and test it before use.
-class LL_COMMON_API LLEventPumps: public LLSingleton<LLEventPumps>,
- public LLHandleProvider<LLEventPumps>
+class LL_COMMON_API LLEventPumps: public LLSingleton<LLEventPumps>
{
LLSINGLETON(LLEventPumps);
public:
@@ -372,56 +356,13 @@ testable:
};
/*****************************************************************************
-* 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.
- */
-typedef boost::signals2::trackable LLEventTrackable;
-
-/*****************************************************************************
* LLEventPump
*****************************************************************************/
/**
* LLEventPump is the base class interface through which we access the
* concrete subclasses such as LLEventStream.
- *
- * @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 LL_COMMON_API LLEventPump: public LLEventTrackable
+class LL_COMMON_API LLEventPump
{
public:
static const std::string ANONYMOUS; // constant for anonymous listeners.
@@ -535,18 +476,37 @@ public:
* call, allows us to optimize away the second and subsequent dependency
* sorts.
*
- * If name is set to LLEventPump::ANONYMOUS listen will bypass the entire
+ * If name is set to LLEventPump::ANONYMOUS, listen() will bypass the entire
* dependency and ordering calculation. In this case, it is critical that
* the result be assigned to a LLTempBoundListener or the listener is
- * manually disconnected when no longer needed since there will be no
+ * manually disconnected when no longer needed, since there will be no
* way to later find and disconnect this listener manually.
*/
+ template <typename LISTENER>
LLBoundListener listen(const std::string& name,
- const LLEventListener& listener,
+ LISTENER&& listener,
const NameList& after=NameList(),
const NameList& before=NameList())
{
- return listen_impl(name, listener, after, before);
+ if constexpr (std::is_invocable_v<LISTENER, const LLSD&>)
+ {
+ // wrap classic LLEventListener in LLAwareListener lambda
+ return listenb(
+ name,
+ [listener=std::move(listener)]
+ (const LLBoundListener&, const LLSD& event)
+ {
+ return listener(event);
+ },
+ after,
+ before);
+ }
+ else
+ {
+ static_assert(std::is_invocable_v<LISTENER, LLBoundListener, const LLSD&>,
+ "LLEventPump::listen() listener has bad parameter signature");
+ return listenb(name, std::forward<LISTENER>(listener), after, before);
+ }
}
/// Get the LLBoundListener associated with the passed name (dummy
@@ -587,17 +547,40 @@ private:
virtual void clear();
virtual void reset();
-
-
private:
- // must precede mName; see LLEventPump::LLEventPump()
- LLHandle<LLEventPumps> mRegistry;
-
std::string mName;
- LLCoros::Mutex mConnectionListMutex;
+ llcoro::Mutex mConnectionListMutex;
protected:
- virtual LLBoundListener listen_impl(const std::string& name, const LLEventListener&,
+ template <typename LISTENER>
+ LLBoundListener listenb(const std::string& name,
+ LISTENER&& listener,
+ const NameList& after=NameList(),
+ const NameList& before=NameList())
+ {
+ using result_t = std::decay_t<decltype(listener(LLBoundListener(), LLSD()))>;
+ if constexpr (std::is_same_v<bool, result_t>)
+ {
+ return listen_impl(name, std::forward<LISTENER>(listener), after, before);
+ }
+ else
+ {
+ static_assert(std::is_same_v<void, result_t>,
+ "LLEventPump::listen() listener has bad return type");
+ // wrap void listener in one that returns bool
+ return listen_impl(
+ name,
+ [listener=std::move(listener)]
+ (const LLBoundListener& conn, const LLSD& event)
+ {
+ listener(conn, event);
+ return false;
+ },
+ after,
+ before);
+ }
+ }
+ virtual LLBoundListener listen_impl(const std::string& name, const LLAwareListener&,
const NameList& after,
const NameList& before);
@@ -672,7 +655,7 @@ public:
void discard();
protected:
- virtual LLBoundListener listen_impl(const std::string& name, const LLEventListener&,
+ virtual LLBoundListener listen_impl(const std::string& name, const LLAwareListener&,
const NameList& after,
const NameList& before) override;
@@ -682,6 +665,30 @@ private:
};
/*****************************************************************************
+* LLNamedListener
+*****************************************************************************/
+/**
+ * LLNamedListener bundles a concrete LLEventPump subclass with a specific
+ * listener function, with an LLTempBoundListener to ensure that it's
+ * disconnected before destruction.
+ */
+template <class PUMP=LLEventStream>
+class LL_COMMON_API LLNamedListener: PUMP
+{
+ using pump_t = PUMP;
+public:
+ template <typename LISTENER>
+ LLNamedListener(const std::string& name, LISTENER&& listener):
+ pump_t(name, false), // don't tweak the name
+ mConn(pump_t::listen("func", std::forward<LISTENER>(listener)))
+ {}
+
+private:
+ LLTempBoundListener mConn;
+};
+using LLStreamListener = LLNamedListener<>;
+
+/*****************************************************************************
* LLReqID
*****************************************************************************/
/**
@@ -773,7 +780,7 @@ private:
* Before sending the reply event, sendReply() copies the ["reqid"] item from
* the request to the reply.
*/
-LL_COMMON_API bool sendReply(const LLSD& reply, const LLSD& request,
+LL_COMMON_API bool sendReply(LLSD reply, const LLSD& request,
const std::string& replyKey="reply");
#endif /* ! defined(LL_LLEVENTS_H) */
diff --git a/indra/llcommon/lleventtimer.cpp b/indra/llcommon/lleventtimer.cpp
index 33fafffefd..cc227193cd 100644
--- a/indra/llcommon/lleventtimer.cpp
+++ b/indra/llcommon/lleventtimer.cpp
@@ -25,49 +25,44 @@
*/
#include "linden_common.h"
-
#include "lleventtimer.h"
-#include "u64.h"
-
-
//////////////////////////////////////////////////////////////////////////////
//
// LLEventTimer Implementation
//
//////////////////////////////////////////////////////////////////////////////
-LLEventTimer::LLEventTimer(F32 period)
-: mEventTimer()
+LLEventTimer::LLEventTimer(F32 period):
+ mPeriod(period)
{
- mPeriod = period;
+ start();
}
-LLEventTimer::LLEventTimer(const LLDate& time)
-: mEventTimer()
+LLEventTimer::LLEventTimer(const LLDate& time):
+ LLEventTimer(F32(time.secondsSinceEpoch() - LLDate::now().secondsSinceEpoch()))
+{}
+
+LLEventTimer::~LLEventTimer()
{
- mPeriod = (F32)(time.secondsSinceEpoch() - LLDate::now().secondsSinceEpoch());
}
-
-LLEventTimer::~LLEventTimer()
+void LLEventTimer::start()
{
+ mTimer = LL::Timers::instance().scheduleEvery([this]{ return tick(); }, mPeriod);
}
-//static
-void LLEventTimer::updateClass()
+void LLEventTimer::stop()
{
- for (auto& timer : instance_snapshot())
- {
- F32 et = timer.mEventTimer.getElapsedTimeF32();
- if (timer.mEventTimer.getStarted() && et > timer.mPeriod) {
- timer.mEventTimer.reset();
- if ( timer.tick() )
- {
- delete &timer;
- }
- }
- }
+ LL::Timers::instance().cancel(mTimer);
}
+bool LLEventTimer::isRunning()
+{
+ return LL::Timers::instance().isRunning(mTimer);
+}
+F32 LLEventTimer::getRemaining()
+{
+ return LL::Timers::instance().timeUntilCall(mTimer);
+}
diff --git a/indra/llcommon/lleventtimer.h b/indra/llcommon/lleventtimer.h
index e0c2381807..b37d682d29 100644
--- a/indra/llcommon/lleventtimer.h
+++ b/indra/llcommon/lleventtimer.h
@@ -27,13 +27,12 @@
#ifndef LL_EVENTTIMER_H
#define LL_EVENTTIMER_H
-#include "stdtypes.h"
+#include "llcallbacklist.h"
#include "lldate.h"
-#include "llinstancetracker.h"
-#include "lltimer.h"
+#include "stdtypes.h"
// class for scheduling a function to be called at a given frequency (approximate, inprecise)
-class LL_COMMON_API LLEventTimer : public LLInstanceTracker<LLEventTimer>
+class LL_COMMON_API LLEventTimer
{
public:
@@ -41,82 +40,18 @@ public:
LLEventTimer(const LLDate& time);
virtual ~LLEventTimer();
+ void start();
+ void stop();
+ bool isRunning();
+ F32 getRemaining();
+
//function to be called at the supplied frequency
- // Normally return FALSE; TRUE will delete the timer after the function returns.
+ // Normally return false; true will delete the timer after the function returns.
virtual bool tick() = 0;
- static void updateClass();
-
- /// Schedule recurring calls to generic callable every period seconds.
- /// Returns a pointer; if you delete it, cancels the recurring calls.
- template <typename CALLABLE>
- static LLEventTimer* run_every(F32 period, const CALLABLE& callable);
-
- /// Schedule a future call to generic callable. Returns a pointer.
- /// CAUTION: The object referenced by that pointer WILL BE DELETED once
- /// the callback has been called! LLEventTimer::getInstance(pointer) (NOT
- /// pointer->getInstance(pointer)!) can be used to test whether the
- /// pointer is still valid. If it is, deleting it will cancel the
- /// callback.
- template <typename CALLABLE>
- static LLEventTimer* run_at(const LLDate& time, const CALLABLE& callable);
-
- /// Like run_at(), but after a time delta rather than at a timestamp.
- /// Same CAUTION.
- template <typename CALLABLE>
- static LLEventTimer* run_after(F32 interval, const CALLABLE& callable);
-
protected:
- LLTimer mEventTimer;
+ LL::Timers::temp_handle_t mTimer;
F32 mPeriod;
-
-private:
- template <typename CALLABLE>
- class Generic;
};
-template <typename CALLABLE>
-class LLEventTimer::Generic: public LLEventTimer
-{
-public:
- // making TIME generic allows engaging either LLEventTimer constructor
- template <typename TIME>
- Generic(const TIME& time, bool once, const CALLABLE& callable):
- LLEventTimer(time),
- mOnce(once),
- mCallable(callable)
- {}
- bool tick() override
- {
- mCallable();
- // true tells updateClass() to delete this instance
- return mOnce;
- }
-
-private:
- bool mOnce;
- CALLABLE mCallable;
-};
-
-template <typename CALLABLE>
-LLEventTimer* LLEventTimer::run_every(F32 period, const CALLABLE& callable)
-{
- // return false to schedule recurring calls
- return new Generic<CALLABLE>(period, false, callable);
-}
-
-template <typename CALLABLE>
-LLEventTimer* LLEventTimer::run_at(const LLDate& time, const CALLABLE& callable)
-{
- // return true for one-shot callback
- return new Generic<CALLABLE>(time, true, callable);
-}
-
-template <typename CALLABLE>
-LLEventTimer* LLEventTimer::run_after(F32 interval, const CALLABLE& callable)
-{
- // one-shot callback after specified interval
- return new Generic<CALLABLE>(interval, true, callable);
-}
-
#endif //LL_EVENTTIMER_H
diff --git a/indra/llcommon/llexception.h b/indra/llcommon/llexception.h
index 68e609444e..15dacd76fc 100644
--- a/indra/llcommon/llexception.h
+++ b/indra/llcommon/llexception.h
@@ -12,6 +12,7 @@
#if ! defined(LL_LLEXCEPTION_H)
#define LL_LLEXCEPTION_H
+#include "stdtypes.h"
#include <stdexcept>
#include <boost/exception/exception.hpp>
#include <boost/throw_exception.hpp>
diff --git a/indra/llcommon/llformat.h b/indra/llcommon/llformat.h
index 4456a72696..0c840d6e8e 100644
--- a/indra/llcommon/llformat.h
+++ b/indra/llcommon/llformat.h
@@ -28,6 +28,8 @@
#ifndef LL_LLFORMAT_H
#define LL_LLFORMAT_H
+#include "llpreprocessor.h"
+
// Use as follows:
// LL_INFOS() << llformat("Test:%d (%.2f %.2f)", idx, x, y) << LL_ENDL;
//
diff --git a/indra/llcommon/llinstancetracker.h b/indra/llcommon/llinstancetracker.h
index 92b26354a1..b5c681e60a 100644
--- a/indra/llcommon/llinstancetracker.h
+++ b/indra/llcommon/llinstancetracker.h
@@ -41,6 +41,7 @@
#include <boost/iterator/indirect_iterator.hpp>
#include <boost/iterator/filter_iterator.hpp>
+#include "llprofiler.h"
#include "lockstatic.h"
#include "stringize.h"
@@ -80,6 +81,8 @@ class LLInstanceTracker
{
InstanceMap mMap;
};
+ // Unfortunately there's no umbrella class that owns all LLInstanceTracker
+ // instances, so there's no good place to call LockStatic::cleanup().
typedef llthread::LockStatic<StaticData> LockStatic;
public:
@@ -170,23 +173,7 @@ public:
}
// lock static data during construction
-#if ! LL_WINDOWS
LockStatic mLock;
-#else // LL_WINDOWS
- // We want to be able to use (e.g.) our instance_snapshot subclass as:
- // for (auto& inst : T::instance_snapshot()) ...
- // But when this snapshot base class directly contains LockStatic, as
- // above, Visual Studio 2017 requires us to code instead:
- // for (auto& inst : std::move(T::instance_snapshot())) ...
- // nat thinks this should be unnecessary, as an anonymous class
- // instance is already a temporary. It shouldn't need to be cast to
- // rvalue reference (the role of std::move()). clang evidently agrees,
- // as the short form works fine with Xcode on Mac.
- // To support the succinct usage, instead of directly storing
- // LockStatic, store std::shared_ptr<LockStatic>, which is copyable.
- std::shared_ptr<LockStatic> mLockp{std::make_shared<LockStatic>()};
- LockStatic& mLock{*mLockp};
-#endif // LL_WINDOWS
VectorType mData;
};
using snapshot = snapshot_of<T>;
@@ -276,6 +263,35 @@ protected:
public:
virtual const KEY& getKey() const { return mInstanceKey; }
+ /// for use ONLY for an object we're sure resides on the heap!
+ static bool erase(const KEY& key)
+ {
+ return erase(getInstance(key));
+ }
+
+ /// for use ONLY for an object we're sure resides on the heap!
+ static bool erase(const weak_t& ptr)
+ {
+ return erase(ptr.lock());
+ }
+
+ /// for use ONLY for an object we're sure resides on the heap!
+ static bool erase(const ptr_t& ptr)
+ {
+ if (! ptr)
+ {
+ return false;
+ }
+
+ // Because we store and return ptr_t instances with no-op deleters,
+ // merely resetting the last pointer doesn't destroy the referenced
+ // object. Don't even bother resetting 'ptr'. Just extract its raw
+ // pointer and delete that.
+ auto raw{ ptr.get() };
+ delete raw;
+ return true;
+ }
+
private:
LLInstanceTracker( const LLInstanceTracker& ) = delete;
LLInstanceTracker& operator=( const LLInstanceTracker& ) = delete;
@@ -356,6 +372,7 @@ class LLInstanceTracker<T, void, KEY_COLLISION_BEHAVIOR>
{
InstanceSet mSet;
};
+ // see LockStatic comment in the above specialization for non-void KEY
typedef llthread::LockStatic<StaticData> LockStatic;
public:
@@ -434,23 +451,7 @@ public:
}
// lock static data during construction
-#if ! LL_WINDOWS
LockStatic mLock;
-#else // LL_WINDOWS
- // We want to be able to use our instance_snapshot subclass as:
- // for (auto& inst : T::instance_snapshot()) ...
- // But when this snapshot base class directly contains LockStatic, as
- // above, Visual Studio 2017 requires us to code instead:
- // for (auto& inst : std::move(T::instance_snapshot())) ...
- // nat thinks this should be unnecessary, as an anonymous class
- // instance is already a temporary. It shouldn't need to be cast to
- // rvalue reference (the role of std::move()). clang evidently agrees,
- // as the short form works fine with Xcode on Mac.
- // To support the succinct usage, instead of directly storing
- // LockStatic, store std::shared_ptr<LockStatic>, which is copyable.
- std::shared_ptr<LockStatic> mLockp{std::make_shared<LockStatic>()};
- LockStatic& mLock{*mLockp};
-#endif // LL_WINDOWS
VectorType mData;
};
using snapshot = snapshot_of<T>;
@@ -481,6 +482,29 @@ public:
template <typename SUBCLASS>
using key_snapshot_of = instance_snapshot_of<SUBCLASS>;
+ /// for use ONLY for an object we're sure resides on the heap!
+ static bool erase(const weak_t& ptr)
+ {
+ return erase(ptr.lock());
+ }
+
+ /// for use ONLY for an object we're sure resides on the heap!
+ static bool erase(const ptr_t& ptr)
+ {
+ if (! ptr)
+ {
+ return false;
+ }
+
+ // Because we store and return ptr_t instances with no-op deleters,
+ // merely resetting the last pointer doesn't destroy the referenced
+ // object. Don't even bother resetting 'ptr'. Just extract its raw
+ // pointer and delete that.
+ auto raw{ ptr.get() };
+ delete raw;
+ return true;
+ }
+
protected:
LLInstanceTracker()
{
diff --git a/indra/llcommon/llinttracker.h b/indra/llcommon/llinttracker.h
new file mode 100644
index 0000000000..fd6d24d0fd
--- /dev/null
+++ b/indra/llcommon/llinttracker.h
@@ -0,0 +1,43 @@
+/**
+ * @file llinttracker.h
+ * @author Nat Goodspeed
+ * @date 2024-08-30
+ * @brief LLIntTracker isa LLInstanceTracker with generated int keys.
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Copyright (c) 2024, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_LLINTTRACKER_H)
+#define LL_LLINTTRACKER_H
+
+#include "llinstancetracker.h"
+
+template <typename T>
+class LLIntTracker: public LLInstanceTracker<T, int>
+{
+ using super = LLInstanceTracker<T, int>;
+public:
+ LLIntTracker():
+ super(getUniqueKey())
+ {}
+
+private:
+ static int getUniqueKey()
+ {
+ // Find a random key that does NOT already correspond to an instance.
+ // Passing a duplicate key to LLInstanceTracker would do Bad Things.
+ int key;
+ do
+ {
+ key = std::rand();
+ } while (super::getInstance(key));
+ // This could be racy, if we were instantiating new LLIntTracker<T>s
+ // on multiple threads. If we need that, have to lock between checking
+ // getInstance() and constructing the new super.
+ return key;
+ }
+};
+
+#endif /* ! defined(LL_LLINTTRACKER_H) */
diff --git a/indra/llcommon/llleap.cpp b/indra/llcommon/llleap.cpp
index 662a2511cd..306d4e48d0 100644
--- a/indra/llcommon/llleap.cpp
+++ b/indra/llcommon/llleap.cpp
@@ -14,8 +14,9 @@
// associated header
#include "llleap.h"
// STL headers
-#include <sstream>
#include <algorithm>
+#include <memory>
+#include <sstream>
// std headers
// external library headers
// other Linden headers
@@ -50,20 +51,19 @@ public:
// We expect multiple LLLeapImpl instances. Definitely tweak
// mDonePump's name for uniqueness.
mDonePump("LLLeap", true),
- // Troubling thought: what if one plugin intentionally messes with
- // another plugin? LLEventPump names are in a single global namespace.
- // Try to make that more difficult by generating a UUID for the reply-
- // pump name -- so it should NOT need tweaking for uniqueness.
- mReplyPump(LLUUID::generateNewID().asString()),
mExpect(0),
// Instantiate a distinct LLLeapListener for this plugin. (Every
// plugin will want its own collection of managed listeners, etc.)
- // Pass it a callback to our connect() method, so it can send events
+ // Pass it our wstdin() method as its callback, so it can send events
// from a particular LLEventPump to the plugin without having to know
// this class or method name.
- mListener(new LLLeapListener(
- [this](LLEventPump& pump, const std::string& listener)
- { return connect(pump, listener); }))
+ mListener(
+ new LLLeapListener(
+ "LLLeap",
+ // Serialize any reply event to our child's stdin, suitably
+ // enriched with the pump name on which it was received.
+ [this](const std::string& pump, const LLSD& data)
+ { return wstdin(pump, data); }))
{
// Rule out unpopulated Params block
if (! cparams.executable.isProvided())
@@ -122,9 +122,6 @@ public:
childout.setLimit(20);
childerr.setLimit(20);
- // Serialize any event received on mReplyPump to our child's stdin.
- mStdinConnection = connect(mReplyPump, "LLLeap");
-
// Listening on stdout is stateful. In general, we're either waiting
// for the length prefix or waiting for the specified length of data.
mReadPrefix = true;
@@ -146,7 +143,7 @@ public:
// Send child a preliminary event reporting our own reply-pump name --
// which would otherwise be pretty tricky to guess!
- wstdin(mReplyPump.getName(),
+ wstdin(mListener->getReplyPump().getName(),
LLSDMap
("command", mListener->getName())
// Include LLLeap features -- this may be important for child to
@@ -193,7 +190,7 @@ public:
return false;
}
- // Listener for events on mReplyPump: send to child stdin
+ // Listener for reply events: send to child stdin
bool wstdin(const std::string& pump, const LLSD& data)
{
LLSD packet(LLSDMap("pump", pump)("data", data));
@@ -344,7 +341,7 @@ public:
{
// The LLSD object we got from our stream contains the
// keys we need.
- LLEventPumps::instance().obtain(data["pump"]).post(data["data"]);
+ LLEventPumps::instance().post(data["pump"], data["data"]);
}
catch (const std::exception& err)
{
@@ -355,7 +352,7 @@ public:
// request, send a reply. We happen to know who originated
// this request, and the reply LLEventPump of interest.
// Not our problem if the plugin ignores the reply event.
- data["reply"] = mReplyPump.getName();
+ data["reply"] = mListener->getReplyPump().getName();
sendReply(llsd::map("error",
stringize(LLError::Log::classname(err), ": ", err.what())),
data);
@@ -423,7 +420,7 @@ public:
LLSD event;
event["type"] = "error";
event["error"] = error;
- mReplyPump.post(event);
+ mListener->getReplyPump().post(event);
// All the above really accomplished was to buffer the serialized
// event in our WritePipe. Have to pump mainloop a couple times to
@@ -440,24 +437,10 @@ public:
}
private:
- /// We always want to listen on mReplyPump with wstdin(); under some
- /// circumstances we'll also echo other LLEventPumps to the plugin.
- LLBoundListener connect(LLEventPump& pump, const std::string& listener)
- {
- // Serialize any event received on the specified LLEventPump to our
- // child's stdin, suitably enriched with the pump name on which it was
- // received.
- return pump.listen(listener,
- [this, name=pump.getName()](const LLSD& data)
- { return wstdin(name, data); });
- }
-
std::string mDesc;
LLEventStream mDonePump;
- LLEventStream mReplyPump;
LLProcessPtr mChild;
- LLTempBoundListener
- mStdinConnection, mStdoutConnection, mStderrConnection;
+ LLTempBoundListener mStdoutConnection, mStderrConnection;
LLProcess::ReadPipe::size_type mExpect;
LLError::RecorderPtr mRecorder;
std::unique_ptr<LLLeapListener> mListener;
diff --git a/indra/llcommon/llleaplistener.cpp b/indra/llcommon/llleaplistener.cpp
index 050d71c327..9742c9e9de 100644
--- a/indra/llcommon/llleaplistener.cpp
+++ b/indra/llcommon/llleaplistener.cpp
@@ -54,53 +54,62 @@
return features;
}
-LLLeapListener::LLLeapListener(const ConnectFunc& connect):
+LLLeapListener::LLLeapListener(std::string_view caller, const Callback& callback):
// Each LEAP plugin has an instance of this listener. Make the command
// pump name difficult for other such plugins to guess.
LLEventAPI(LLUUID::generateNewID().asString(),
"Operations relating to the LLSD Event API Plugin (LEAP) protocol"),
- mConnect(connect)
+ mCaller(caller),
+ mCallback(callback),
+ // Troubling thought: what if one plugin intentionally messes with
+ // another plugin? LLEventPump names are in a single global namespace.
+ // Try to make that more difficult by generating a UUID for the reply-
+ // pump name -- so it should NOT need tweaking for uniqueness.
+ mReplyPump(LLUUID::generateNewID().asString()),
+ mReplyConn(connect(mReplyPump, mCaller))
{
LLSD need_name(LLSDMap("name", LLSD()));
add("newpump",
- "Instantiate a new LLEventPump named like [\"name\"] and listen to it.\n"
- "[\"type\"] == \"LLEventStream\", \"LLEventMailDrop\" et al.\n"
- "Events sent through new LLEventPump will be decorated with [\"pump\"]=name.\n"
- "Returns actual name in [\"name\"] (may be different if collision).",
+R"-(Instantiate a new LLEventPump named like ["name"] and listen to it.
+["type"] == "LLEventStream", "LLEventMailDrop" et al.
+Events sent through new LLEventPump will be decorated with ["pump"]=name.
+Returns actual name in ["name"] (may be different if collision).)-",
&LLLeapListener::newpump,
need_name);
LLSD need_source_listener(LLSDMap("source", LLSD())("listener", LLSD()));
add("listen",
- "Listen to an existing LLEventPump named [\"source\"], with listener name\n"
- "[\"listener\"].\n"
- "By default, send events on [\"source\"] to the plugin, decorated\n"
- "with [\"pump\"]=[\"source\"].\n"
- "If [\"dest\"] specified, send undecorated events on [\"source\"] to the\n"
- "LLEventPump named [\"dest\"].\n"
- "Returns [\"status\"] boolean indicating whether the connection was made.",
+R"-(Listen to an existing LLEventPump named ["source"], with listener name
+["listener"].
+If ["tweak"] is specified as true, tweak listener name for uniqueness.
+By default, send events on ["source"] to the plugin, decorated
+with ["pump"]=["source"].
+If ["dest"] specified, send undecorated events on ["source"] to the
+LLEventPump named ["dest"].
+Returns ["status"] boolean indicating whether the connection was made,
+plus ["listener"] reporting (possibly tweaked) listener name.)-",
&LLLeapListener::listen,
need_source_listener);
add("stoplistening",
- "Disconnect a connection previously established by \"listen\".\n"
- "Pass same [\"source\"] and [\"listener\"] arguments.\n"
- "Returns [\"status\"] boolean indicating whether such a listener existed.",
+R"-(Disconnect a connection previously established by "listen".
+Pass same ["source"] and ["listener"] arguments.
+Returns ["status"] boolean indicating whether such a listener existed.)-",
&LLLeapListener::stoplistening,
need_source_listener);
add("ping",
- "No arguments, just a round-trip sanity check.",
+"No arguments, just a round-trip sanity check.",
&LLLeapListener::ping);
add("getAPIs",
- "Enumerate all LLEventAPI instances by name and description.",
+"Enumerate all LLEventAPI instances by name and description.",
&LLLeapListener::getAPIs);
add("getAPI",
- "Get name, description, dispatch key and operations for LLEventAPI [\"api\"].",
+R"-(Get name, description, dispatch key and operations for LLEventAPI ["api"].)-",
&LLLeapListener::getAPI,
LLSD().with("api", LLSD()));
add("getFeatures",
- "Return an LLSD map of feature strings (deltas from baseline LEAP protocol)",
+"Return an LLSD map of feature strings (deltas from baseline LEAP protocol)",
static_cast<void (LLLeapListener::*)(const LLSD&) const>(&LLLeapListener::getFeatures));
add("getFeature",
- "Return the feature value with key [\"feature\"]",
+R"-(Return the feature value with key ["feature"])-",
&LLLeapListener::getFeature,
LLSD().with("feature", LLSD()));
}
@@ -112,6 +121,7 @@ LLLeapListener::~LLLeapListener()
// value_type, and Bad Things would happen if you copied an
// LLTempBoundListener. (Destruction of the original would disconnect the
// listener, invalidating every stored connection.)
+ LL_DEBUGS("LLLeapListener") << "~LLLeapListener(\"" << mCaller << "\")" << LL_ENDL;
for (ListenersMap::value_type& pair : mListeners)
{
pair.second.disconnect();
@@ -133,8 +143,7 @@ void LLLeapListener::newpump(const LLSD& request)
reply["name"] = name;
// Now listen on this new pump with our plugin listener
- std::string myname("llleap");
- saveListener(name, myname, mConnect(new_pump, myname));
+ saveListener(name, mCaller, connect(new_pump, mCaller));
}
catch (const LLEventPumps::BadType& error)
{
@@ -149,6 +158,11 @@ void LLLeapListener::listen(const LLSD& request)
std::string source_name = request["source"];
std::string dest_name = request["dest"];
std::string listener_name = request["listener"];
+ if (request["tweak"].asBoolean())
+ {
+ listener_name = LLEventPump::inventName(listener_name);
+ }
+ reply["listener"] = listener_name;
LLEventPump & source = LLEventPumps::instance().obtain(source_name);
@@ -170,7 +184,7 @@ void LLLeapListener::listen(const LLSD& request)
{
// "dest" unspecified means to direct events on "source"
// to our plugin listener.
- saveListener(source_name, listener_name, mConnect(source, listener_name));
+ saveListener(source_name, listener_name, connect(source, listener_name));
}
reply["status"] = true;
}
@@ -292,10 +306,27 @@ void LLLeapListener::getFeature(const LLSD& request) const
}
}
+LLBoundListener LLLeapListener::connect(LLEventPump& pump, const std::string& listener)
+{
+ // Connect to source pump with an adapter that calls our callback with the
+ // pump name as well as the event data.
+ return pump.listen(
+ listener,
+ [callback=mCallback, pump=pump.getName()]
+ (const LLSD& data)
+ { return callback(pump, data); });
+}
+
void LLLeapListener::saveListener(const std::string& pump_name,
const std::string& listener_name,
const LLBoundListener& listener)
{
- mListeners.insert(ListenersMap::value_type(ListenersMap::key_type(pump_name, listener_name),
- listener));
+ // Don't use insert() or emplace() because, if this (pump_name,
+ // listener_name) pair is already in mListeners, we *want* to overwrite it.
+ auto& listener_entry{ mListeners[ListenersMap::key_type(pump_name, listener_name)] };
+ // If we already stored a connection for this pump and listener name,
+ // disconnect it before overwriting it. But if this entry was newly
+ // created, disconnect() will be a no-op.
+ listener_entry.disconnect();
+ listener_entry = listener;
}
diff --git a/indra/llcommon/llleaplistener.h b/indra/llcommon/llleaplistener.h
index cad4543d02..d38a6f4ace 100644
--- a/indra/llcommon/llleaplistener.h
+++ b/indra/llcommon/llleaplistener.h
@@ -13,10 +13,9 @@
#define LL_LLLEAPLISTENER_H
#include "lleventapi.h"
+#include <functional>
#include <map>
#include <string>
-#include <boost/function.hpp>
-#include <boost/ptr_container/ptr_map.hpp>
/// Listener class implementing LLLeap query/control operations.
/// See https://jira.lindenlab.com/jira/browse/DEV-31978.
@@ -24,18 +23,16 @@ class LLLeapListener: public LLEventAPI
{
public:
/**
- * Decouple LLLeap by dependency injection. Certain LLLeapListener
- * operations must be able to cause LLLeap to listen on a specified
- * LLEventPump with the LLLeap listener that wraps incoming events in an
- * outer (pump=, data=) map and forwards them to the plugin. Very well,
- * define the signature for a function that will perform that, and make
- * our constructor accept such a function.
+ * Certain LLLeapListener operations listen on a specified LLEventPump.
+ * Accept a bool(pump, data) callback from our caller for when any such
+ * event is received.
*/
- typedef boost::function<LLBoundListener(LLEventPump&, const std::string& listener)>
- ConnectFunc;
- LLLeapListener(const ConnectFunc& connect);
+ using Callback = std::function<bool(const std::string& pump, const LLSD& data)>;
+ LLLeapListener(std::string_view caller, const Callback& callback);
~LLLeapListener();
+ LLEventPump& getReplyPump() { return mReplyPump; }
+
static LLSD getFeatures();
private:
@@ -48,10 +45,16 @@ private:
void getFeatures(const LLSD&) const;
void getFeature(const LLSD&) const;
+ LLBoundListener connect(LLEventPump& pump, const std::string& listener);
void saveListener(const std::string& pump_name, const std::string& listener_name,
const LLBoundListener& listener);
- ConnectFunc mConnect;
+ // The relative order of these next declarations is important because the
+ // constructor will initialize in this order.
+ std::string mCaller;
+ Callback mCallback;
+ LLEventStream mReplyPump;
+ LLTempBoundListener mReplyConn;
// In theory, listen() could simply call the relevant LLEventPump's
// listen() method, stoplistening() likewise. Lifespan issues make us
diff --git a/indra/llcommon/lllivefile.cpp b/indra/llcommon/lllivefile.cpp
index 58de61a7e4..b20c91dbe4 100644
--- a/indra/llcommon/lllivefile.cpp
+++ b/indra/llcommon/lllivefile.cpp
@@ -170,7 +170,7 @@ namespace
: LLEventTimer(refresh), mLiveFile(f)
{ }
- bool tick()
+ bool tick() override
{
mLiveFile.checkAndReload();
return false;
diff --git a/indra/llcommon/llmainthreadtask.h b/indra/llcommon/llmainthreadtask.h
index c3ed7fef52..eccf11fcbe 100644
--- a/indra/llcommon/llmainthreadtask.h
+++ b/indra/llcommon/llmainthreadtask.h
@@ -13,11 +13,8 @@
#if ! defined(LL_LLMAINTHREADTASK_H)
#define LL_LLMAINTHREADTASK_H
-#include "lleventtimer.h"
#include "llthread.h"
-#include "llmake.h"
-#include <future>
-#include <type_traits> // std::result_of
+#include "workqueue.h"
/**
* LLMainThreadTask provides a way to perform some task specifically on the
@@ -28,18 +25,17 @@
* Instead of instantiating LLMainThreadTask, pass your invocable to its
* static dispatch() method. dispatch() returns the result of calling your
* task. (Or, if your task throws an exception, dispatch() throws that
- * exception. See std::packaged_task.)
+ * exception.)
*
* When you call dispatch() on the main thread (as determined by
* on_main_thread() in llthread.h), it simply calls your task and returns the
* result.
*
- * When you call dispatch() on a secondary thread, it instantiates an
- * LLEventTimer subclass scheduled immediately. Next time the main loop calls
- * LLEventTimer::updateClass(), your task will be run, and LLMainThreadTask
- * will fulfill a future with its result. Meanwhile the requesting thread
- * blocks on that future. As soon as it is set, the requesting thread wakes up
- * with the task result.
+ * When you call dispatch() on a secondary thread, it posts your task to
+ * gMainloopWork, the WorkQueue serviced by the main thread, using
+ * WorkQueue::waitForResult() to block the caller. Next time the main loop
+ * calls gMainloopWork.runFor(), your task will be run, and waitForResult()
+ * will return its result.
*/
class LLMainThreadTask
{
@@ -59,41 +55,15 @@ public:
}
else
{
- // It's essential to construct LLEventTimer subclass instances on
- // the heap because, on completion, LLEventTimer deletes them.
- // Once we enable C++17, we can use Class Template Argument
- // Deduction. Until then, use llmake_heap().
- auto* task = llmake_heap<Task>(std::forward<CALLABLE>(callable));
- auto future = task->mTask.get_future();
- // Now simply block on the future.
- return future.get();
+ auto queue{ LL::WorkQueue::getInstance("mainloop") };
+ // If this needs a null check and a message, please introduce a
+ // method in the .cpp file so consumers of this header don't drag
+ // in llerror.h.
+ // Use waitForResult_() so dispatch() can be used even from the
+ // calling thread's default coroutine.
+ return queue->waitForResult_(std::forward<CALLABLE>(callable));
}
}
-
-private:
- template <typename CALLABLE>
- struct Task: public LLEventTimer
- {
- Task(CALLABLE&& callable):
- // no wait time: call tick() next chance we get
- LLEventTimer(0),
- mTask(std::forward<CALLABLE>(callable))
- {}
- bool tick() override
- {
- // run the task on the main thread, will populate the future
- // obtained by get_future()
- mTask();
- // tell LLEventTimer we're done (one shot)
- return true;
- }
- // Given arbitrary CALLABLE, which might be a lambda, how are we
- // supposed to obtain its signature for std::packaged_task? It seems
- // redundant to have to add an argument list to engage invoke_result_t, then
- // add the argument list again to complete the signature. At least we
- // only support a nullary CALLABLE.
- std::packaged_task<std::invoke_result_t<CALLABLE>()> mTask;
- };
};
#endif /* ! defined(LL_LLMAINTHREADTASK_H) */
diff --git a/indra/llcommon/llrefcount.h b/indra/llcommon/llrefcount.h
index 3a253d8fa6..707d825e53 100644
--- a/indra/llcommon/llrefcount.h
+++ b/indra/llcommon/llrefcount.h
@@ -29,6 +29,7 @@
#include <boost/noncopyable.hpp>
#include <boost/intrusive_ptr.hpp>
#include "llatomic.h"
+#include "llerror.h"
class LLMutex;
diff --git a/indra/llcommon/llrun.h b/indra/llcommon/llrun.h
index 70767572ff..adf1f49001 100644
--- a/indra/llcommon/llrun.h
+++ b/indra/llcommon/llrun.h
@@ -33,6 +33,17 @@
class LLRunnable;
+//////////////////////////////////////////////////////////////////////////////
+// DEPRECATION WARNING
+// LLRunner is one of several mostly redundant ways to schedule future
+// callbacks on the main thread. It seems to be unused in the current viewer.
+// addRunnable() is only called by LLPumpIO::sleepChain().
+// sleepChain() is only called by LLIOSleeper and LLIOSleep.
+// LLIOSleeper is referenced only by tests.
+// LLIOSleep is only called by LLDeferredChain.
+// LLDeferredChain isn't referenced at all.
+//////////////////////////////////////////////////////////////////////////////
+
/**
* @class LLRunner
* @brief This class manages a set of LLRunnable objects.
diff --git a/indra/llcommon/llsdutil.h b/indra/llcommon/llsdutil.h
index 38bbe19ddd..3ce0b1726d 100644
--- a/indra/llcommon/llsdutil.h
+++ b/indra/llcommon/llsdutil.h
@@ -365,15 +365,14 @@ private:
// subject function has returned, so we must ensure that any constructed
// LLSDParam<T> lives just as long as this LLSDParam<LLSD> does. Putting
// each LLSDParam<T> on the heap and capturing a smart pointer in a vector
- // works. We would have liked to use std::unique_ptr, but vector entries
- // must be copyable.
+ // works.
// (Alternatively we could assume that every instance of LLSDParam<LLSD>
// will be asked for at most ONE conversion. We could store a scalar
// std::unique_ptr and, when constructing an new LLSDParam<T>, assert that
// the unique_ptr is empty. But some future change in usage patterns, and
// consequent failure of that assertion, would be very mysterious. Instead
// of explaining how to fix it, just fix it now.)
- mutable std::vector<std::shared_ptr<LLSDParamBase>> converters_;
+ mutable std::vector<std::unique_ptr<LLSDParamBase>> converters_;
public:
LLSDParam(const LLSD& value): value_(value) {}
@@ -389,9 +388,9 @@ public:
{
// capture 'ptr' with the specific subclass type because converters_
// only stores LLSDParamBase pointers
- auto ptr{ std::make_shared<LLSDParam<std::decay_t<T>>>(value_) };
+ auto ptr{ new LLSDParam<std::decay_t<T>>(value_) };
// keep the new converter alive until we ourselves are destroyed
- converters_.push_back(ptr);
+ converters_.emplace_back(ptr);
return *ptr;
}
};
@@ -474,12 +473,12 @@ public:
}
};
-namespace llsd
-{
-
/*****************************************************************************
* range-based for-loop helpers for LLSD
*****************************************************************************/
+namespace llsd
+{
+
/// Usage: for (LLSD item : inArray(someLLSDarray)) { ... }
class inArray
{
@@ -525,7 +524,70 @@ private:
} // namespace llsd
+/*****************************************************************************
+* LLSDParam<std::vector<T>>
+*****************************************************************************/
+// Given an LLSD array, return a const std::vector<T>&, where T is a type
+// supported by LLSDParam. Bonus: if the LLSD value is actually a scalar,
+// return a single-element vector containing the converted value.
+template <typename T>
+class LLSDParam<std::vector<T>>: public LLSDParamBase
+{
+public:
+ LLSDParam(const LLSD& array)
+ {
+ // treat undefined "array" as empty vector
+ if (array.isDefined())
+ {
+ // what if it's a scalar?
+ if (! array.isArray())
+ {
+ v.push_back(LLSDParam<T>(array));
+ }
+ else // really is an array
+ {
+ // reserve space for the array entries
+ v.reserve(array.size());
+ for (const auto& item : llsd::inArray(array))
+ {
+ v.push_back(LLSDParam<T>(item));
+ }
+ }
+ }
+ }
+
+ operator const std::vector<T>&() const { return v; }
+
+private:
+ std::vector<T> v;
+};
+
+/*****************************************************************************
+* LLSDParam<std::map<std::string, T>>
+*****************************************************************************/
+// Given an LLSD map, return a const std::map<std::string, T>&, where T is a
+// type supported by LLSDParam.
+template <typename T>
+class LLSDParam<std::map<std::string, T>>: public LLSDParamBase
+{
+public:
+ LLSDParam(const LLSD& map)
+ {
+ for (const auto& pair : llsd::inMap(map))
+ {
+ m[pair.first] = LLSDParam<T>(pair.second);
+ }
+ }
+
+ operator const std::map<std::string, T>&() const { return m; }
+
+private:
+ std::map<std::string, T> m;
+};
+/*****************************************************************************
+* deep and shallow clone
+*****************************************************************************/
// Creates a deep clone of an LLSD object. Maps, Arrays and binary objects
// are duplicated, atomic primitives (Boolean, Integer, Real, etc) simply
// use a shared reference.
@@ -553,6 +615,60 @@ LLSD shallow(LLSD value, LLSD filter=LLSD()) { return llsd_shallow(value, filter
} // namespace llsd
+/*****************************************************************************
+* toArray(), toMap()
+*****************************************************************************/
+namespace llsd
+{
+
+// For some T convertible to LLSD, given std::vector<T> myVec,
+// toArray(myVec) returns an LLSD array whose entries correspond to the
+// items in myVec.
+// For some U convertible to LLSD, given function U xform(const T&),
+// toArray(myVec, xform) returns an LLSD array whose every entry is
+// xform(item) of the corresponding item in myVec.
+// toArray() actually works with any container<C> usable with range
+// 'for', not just std::vector.
+// (Once we get C++20 we can use std::identity instead of this default lambda.)
+template <typename C, typename FUNC>
+LLSD toArray(const C& container, FUNC&& func=[](const auto& arg){ return arg; })
+{
+ LLSD array;
+ for (const auto& item : container)
+ {
+ array.append(std::forward<FUNC>(func)(item));
+ }
+ return array;
+}
+
+// For some T convertible to LLSD, given std::map<std::string, T> myMap,
+// toMap(myMap) returns an LLSD map whose entries correspond to the
+// (key, value) pairs in myMap.
+// For some U convertible to LLSD, given function
+// std::pair<std::string, U> xform(const std::pair<std::string, T>&),
+// toMap(myMap, xform) returns an LLSD map whose every entry is
+// xform(pair) of the corresponding (key, value) pair in myMap.
+// toMap() actually works with any container usable with range 'for', not
+// just std::map. It need not even be an associative container, as long as
+// you pass an xform function that returns std::pair<std::string, U>.
+// (Once we get C++20 we can use std::identity instead of this default lambda.)
+template <typename C, typename FUNC>
+LLSD toMap(const C& container, FUNC&& func=[](const auto& arg){ return arg; })
+{
+ LLSD map;
+ for (const auto& pair : container)
+ {
+ const auto& [key, value] = std::forward<FUNC>(func)(pair);
+ map[key] = value;
+ }
+ return map;
+}
+
+} // namespace llsd
+
+/*****************************************************************************
+* boost::hash<LLSD>
+*****************************************************************************/
// Specialization for generating a hash value from an LLSD block.
namespace boost
{
diff --git a/indra/llcommon/llsingleton.cpp b/indra/llcommon/llsingleton.cpp
index 05dc3cde79..2a38d6d896 100644
--- a/indra/llcommon/llsingleton.cpp
+++ b/indra/llcommon/llsingleton.cpp
@@ -27,11 +27,12 @@
#include "linden_common.h"
#include "llsingleton.h"
+#include "llcoros.h"
+#include "lldependencies.h"
#include "llerror.h"
#include "llerrorcontrol.h"
-#include "lldependencies.h"
#include "llexception.h"
-#include "llcoros.h"
+#include "llmainthreadtask.h"
#include <algorithm>
#include <iostream> // std::cerr in dire emergency
#include <sstream>
@@ -270,17 +271,29 @@ void LLSingletonBase::reset_initializing(list_t::size_type size)
void LLSingletonBase::MasterList::LockedInitializing::log(const char* verb, const char* name)
{
- LL_DEBUGS("LLSingleton") << verb << ' ' << demangle(name) << ';';
- if (mList)
+ LL_DEBUGS("LLSingleton") << verb << ' ' << demangle(name) << ';';
+ if (mList)
+ {
+ for (list_t::const_reverse_iterator ri(mList->rbegin()), rend(mList->rend());
+ ri != rend; ++ri)
{
- for (list_t::const_reverse_iterator ri(mList->rbegin()), rend(mList->rend());
- ri != rend; ++ri)
- {
- LLSingletonBase* sb(*ri);
- LL_CONT << ' ' << classname(sb);
- }
+ LLSingletonBase* sb(*ri);
+ LL_CONT << ' ' << classname(sb);
}
- LL_ENDL;
+ }
+ LL_ENDL;
+}
+
+void LLSingletonBase::capture_dependency(LLSingletonBase* sb)
+{
+ // If we're called very late during application shutdown, the Boost.Fibers
+ // library may have shut down, and MasterList::mInitializing.get() might
+ // blow up. But if we're called that late, there's really no point in
+ // trying to capture this dependency.
+ if (boost::fibers::context::active())
+ {
+ sb->capture_dependency();
+ }
}
void LLSingletonBase::capture_dependency()
@@ -484,3 +497,29 @@ std::string LLSingletonBase::demangle(const char* mangled)
{
return LLError::Log::demangle(mangled);
}
+
+LLSingletonBase* LLSingletonBase::getInstanceForSecondaryThread(
+ const std::string& name,
+ const std::string& method,
+ const std::function<LLSingletonBase*()>& getInstance)
+{
+ // Normally it would be the height of folly to reference-bind args into a
+ // lambda to be executed on some other thread! By the time that thread
+ // executed the lambda, the references would all be dangling, and Bad
+ // Things would result. But LLMainThreadTask::dispatch() promises to block
+ // the calling thread until the passed task has completed. So in this case
+ // we know the references will remain valid until the lambda has run, so
+ // we dare to bind references.
+ return LLMainThreadTask::dispatch(
+ [&name, &method, &getInstance](){
+ // VERY IMPORTANT to call getInstance() on the main thread,
+ // rather than going straight to constructSingleton()!
+ // During the time window before mInitState is INITIALIZED,
+ // multiple requests might be queued. It's essential that, as
+ // the main thread processes them, only the FIRST such request
+ // actually constructs the instance -- every subsequent one
+ // simply returns the existing instance.
+ loginfos({name, "::", method, " on main thread"});
+ return getInstance();
+ });
+}
diff --git a/indra/llcommon/llsingleton.h b/indra/llcommon/llsingleton.h
index b5659e053c..d3417a233e 100644
--- a/indra/llcommon/llsingleton.h
+++ b/indra/llcommon/llsingleton.h
@@ -25,18 +25,19 @@
#ifndef LLSINGLETON_H
#define LLSINGLETON_H
+#include <boost/call_traits.hpp>
#include <boost/noncopyable.hpp>
#include <boost/unordered_set.hpp>
+#include <functional>
#include <initializer_list>
#include <list>
#include <typeinfo>
#include <vector>
#include "mutex.h"
#include "lockstatic.h"
-#include "llthread.h" // on_main_thread()
-#include "llmainthreadtask.h"
+#include "apply.h"
#include "llprofiler.h"
-#include "llerror.h"
+#include "llthread.h" // on_main_thread()
#ifdef LL_WINDOWS
#pragma warning(push)
@@ -117,6 +118,8 @@ protected:
// If a given call to B::getInstance() happens during either A::A() or
// A::initSingleton(), record that A directly depends on B.
void capture_dependency();
+ // trampoline to non-static member function
+ static void capture_dependency(LLSingletonBase*);
// delegate logging calls to llsingleton.cpp
public:
@@ -141,6 +144,17 @@ protected:
// internal wrapper around calls to cleanupSingleton()
void cleanup_();
+ // This method is where we dispatch to LLMainThreadTask to acquire the
+ // subclass LLSingleton instance when the first getInstance() call is from
+ // a secondary thread. We delegate to the .cpp file to untangle header
+ // circularity. It accepts a std::function referencing the subclass
+ // getInstance() method -- which can't be virtual because it's static; we
+ // don't yet have an instance! For messaging, it also accepts the name of
+ // the subclass and the subclass method.
+ static LLSingletonBase* getInstanceForSecondaryThread(
+ const std::string& name, const std::string& method,
+ const std::function<LLSingletonBase*()>& getInstance);
+
// deleteSingleton() isn't -- and shouldn't be -- a virtual method. It's a
// class static. However, given only Foo*, deleteAll() does need to be
// able to reach Foo::deleteSingleton(). Make LLSingleton (which declares
@@ -196,7 +210,7 @@ struct LLSingleton_manage_master
}
void capture_dependency(LLSingletonBase* sb)
{
- sb->capture_dependency();
+ LLSingletonBase::capture_dependency(sb);
}
};
@@ -428,6 +442,11 @@ protected:
// Remove this instance from the master list.
LLSingleton_manage_master<DERIVED_TYPE>().remove(this);
+ // We should no longer need our LockStatic -- but the fact that we can
+ // query or even resurrect a deleted LLSingleton means we basically
+ // have to shrug and leak our SingletonData. It's not large, and this
+ // only happens at shutdown anyway.
+// lk.cleanup();
}
public:
@@ -563,19 +582,11 @@ public:
// Per the comment block above, dispatch to the main thread.
loginfos({classname<DERIVED_TYPE>(),
"::getInstance() dispatching to main thread"});
- auto instance = LLMainThreadTask::dispatch(
- [](){
- // VERY IMPORTANT to call getInstance() on the main thread,
- // rather than going straight to constructSingleton()!
- // During the time window before mInitState is INITIALIZED,
- // multiple requests might be queued. It's essential that, as
- // the main thread processes them, only the FIRST such request
- // actually constructs the instance -- every subsequent one
- // simply returns the existing instance.
- loginfos({classname<DERIVED_TYPE>(),
- "::getInstance() on main thread"});
- return getInstance();
- });
+ auto instance = static_cast<DERIVED_TYPE*>(
+ getInstanceForSecondaryThread(
+ classname<DERIVED_TYPE>(),
+ "getInstance()",
+ getInstance));
// record the dependency chain tracked on THIS thread, not the main
// thread (consider a getInstance() overload with a tag param that
// suppresses dep tracking when dispatched to the main thread)
@@ -640,8 +651,14 @@ private:
// Passes arguments to DERIVED_TYPE's constructor and sets appropriate
// states, returning a pointer to the new instance.
+ // If we just let initParamSingleton_() infer its argument types, the
+ // compiler has trouble passing int and string literals. Use
+ // boost::call_traits::param_type to smooth parameter passing. This
+ // construction requires, though, that each invocation of this method
+ // explicitly specify template arguments, instead of inferring them.
template <typename... Args>
- static DERIVED_TYPE* initParamSingleton_(Args&&... args)
+ static LLSingletonBase* initParamSingleton_(
+ typename boost::call_traits<Args>::param_type... args)
{
// In case racing threads both call initParamSingleton() at the same
// time, serialize them. One should initialize; the other should see
@@ -660,7 +677,7 @@ private:
// on the main thread, simply construct instance while holding lock
super::logdebugs({super::template classname<DERIVED_TYPE>(),
"::initParamSingleton()"});
- super::constructSingleton(lk, std::forward<Args>(args)...);
+ super::constructSingleton(lk, args...);
return lk->mInstance;
}
else
@@ -673,20 +690,19 @@ private:
lk.unlock();
super::loginfos({super::template classname<DERIVED_TYPE>(),
"::initParamSingleton() dispatching to main thread"});
- // Normally it would be the height of folly to reference-bind
- // 'args' into a lambda to be executed on some other thread! By
- // the time that thread executed the lambda, the references would
- // all be dangling, and Bad Things would result. But
- // LLMainThreadTask::dispatch() promises to block until the passed
- // task has completed. So in this case we know the references will
- // remain valid until the lambda has run, so we dare to bind
- // references.
- auto instance = LLMainThreadTask::dispatch(
- [&](){
- super::loginfos({super::template classname<DERIVED_TYPE>(),
- "::initParamSingleton() on main thread"});
- return initParamSingleton_(std::forward<Args>(args)...);
- });
+ auto instance = static_cast<DERIVED_TYPE*>(
+ super::getInstanceForSecondaryThread(
+ super::template classname<DERIVED_TYPE>(),
+ "initParamSingleton()",
+ // This lambda does what std::bind() is supposed to do --
+ // but when the actual parameter is (e.g.) a string
+ // literal, type inference makes it fail. Apply param_type
+ // to each incoming type to make it work.
+ [args=std::tuple<typename boost::call_traits<Args>::param_type...>(args...)]
+ ()
+ {
+ return LL::apply(initParamSingleton_<Args...>, args);
+ }));
super::loginfos({super::template classname<DERIVED_TYPE>(),
"::initParamSingleton() returning on requesting thread"});
return instance;
@@ -703,7 +719,7 @@ public:
template <typename... Args>
static DERIVED_TYPE& initParamSingleton(Args&&... args)
{
- return *initParamSingleton_(std::forward<Args>(args)...);
+ return *static_cast<DERIVED_TYPE*>(initParamSingleton_<Args...>(args...));
}
static DERIVED_TYPE* getInstance()
diff --git a/indra/llcommon/llstring.cpp b/indra/llcommon/llstring.cpp
index 07adf71d18..447fa2d9dd 100644
--- a/indra/llcommon/llstring.cpp
+++ b/indra/llcommon/llstring.cpp
@@ -31,6 +31,7 @@
#include "llfasttimer.h"
#include "llsd.h"
#include <vector>
+#include <sstream>
#if LL_WINDOWS
#include "llwin32headers.h"
@@ -140,10 +141,10 @@ std::string rawstr_to_utf8(const std::string& raw)
return wstring_to_utf8str(wstr);
}
-std::ptrdiff_t wchar_to_utf8chars(llwchar in_char, char* outchars)
+std::string wchar_to_utf8chars(llwchar in_char)
{
- U32 cur_char = (U32)in_char;
- char* base = outchars;
+ U32 cur_char(in_char);
+ char buff[8], *outchars = buff;
if (cur_char < 0x80)
{
*outchars++ = (U8)cur_char;
@@ -188,7 +189,7 @@ std::ptrdiff_t wchar_to_utf8chars(llwchar in_char, char* outchars)
LL_WARNS() << "Invalid Unicode character " << cur_char << "!" << LL_ENDL;
*outchars++ = LL_UNKNOWN_CHAR;
}
- return outchars - base;
+ return { buff, std::string::size_type(outchars - buff) };
}
auto utf16chars_to_wchar(const U16* inchars, llwchar* outchar)
@@ -213,7 +214,8 @@ auto utf16chars_to_wchar(const U16* inchars, llwchar* outchar)
llutf16string wstring_to_utf16str(const llwchar* utf32str, size_t len)
{
- llutf16string out;
+ // ostringstream for llutf16string
+ std::basic_ostringstream<U16> out;
S32 i = 0;
while (i < len)
@@ -221,16 +223,16 @@ llutf16string wstring_to_utf16str(const llwchar* utf32str, size_t len)
U32 cur_char = utf32str[i];
if (cur_char > 0xFFFF)
{
- out += (0xD7C0 + (cur_char >> 10));
- out += (0xDC00 | (cur_char & 0x3FF));
+ out.put(U16(0xD7C0 + (cur_char >> 10)));
+ out.put(U16(0xDC00 | (cur_char & 0x3FF)));
}
else
{
- out += cur_char;
+ out.put(U16(cur_char));
}
i++;
}
- return out;
+ return out.str();
}
llutf16string utf8str_to_utf16str( const char* utf8str, size_t len )
@@ -241,8 +243,16 @@ llutf16string utf8str_to_utf16str( const char* utf8str, size_t len )
LLWString utf16str_to_wstring(const U16* utf16str, size_t len)
{
- LLWString wout;
- if (len == 0) return wout;
+ if (len == 0) return {};
+
+ // MS doesn't support std::basic_ostringstream<llwchar>; have to work
+ // around it.
+ std::vector<llwchar> wout;
+ // We want to minimize allocations. We don't know how many llwchars we'll
+ // generate from this utf16str, but we do know the length should be at
+ // most len. So if we reserve 'len' llwchars, we shouldn't need to expand
+ // wout incrementally.
+ wout.reserve(len);
S32 i = 0;
const U16* chars16 = utf16str;
@@ -250,9 +260,9 @@ LLWString utf16str_to_wstring(const U16* utf16str, size_t len)
{
llwchar cur_char;
i += (S32)utf16chars_to_wchar(chars16+i, &cur_char);
- wout += cur_char;
+ wout.push_back(cur_char);
}
- return wout;
+ return { wout.begin(), wout.end() };
}
// Length in llwchar (UTF-32) of the first len units (16 bits) of the given UTF-16 string.
@@ -366,13 +376,12 @@ std::string wchar_utf8_preview(const llwchar wc)
std::ostringstream oss;
oss << std::hex << std::uppercase << (U32)wc;
- U8 out_bytes[8];
- U32 size = (U32)wchar_to_utf8chars(wc, (char*)out_bytes);
+ auto out_bytes = wchar_to_utf8chars(wc);
- if (size > 1)
+ if (out_bytes.length() > 1)
{
oss << " [";
- for (U32 i = 0; i < size; ++i)
+ for (U32 i = 0; i < out_bytes.length(); ++i)
{
if (i)
{
@@ -398,7 +407,14 @@ S32 wstring_utf8_length(const LLWString& wstr)
LLWString utf8str_to_wstring(const char* utf8str, size_t len)
{
- LLWString wout;
+ // MS doesn't support std::basic_ostringstream<llwchar>; have to work
+ // around it.
+ std::vector<llwchar> wout;
+ // We want to minimize allocations. We don't know how many llwchars we'll
+ // generate from this utf8str, but we do know the length should be at most
+ // len. So if we reserve 'len' llwchars, we shouldn't need to expand wout
+ // incrementally.
+ wout.reserve(len);
S32 i = 0;
while (i < len)
@@ -441,7 +457,7 @@ LLWString utf8str_to_wstring(const char* utf8str, size_t len)
}
else
{
- wout += LL_UNKNOWN_CHAR;
+ wout.push_back(LL_UNKNOWN_CHAR);
++i;
continue;
}
@@ -478,26 +494,21 @@ LLWString utf8str_to_wstring(const char* utf8str, size_t len)
}
}
- wout += unichar;
+ wout.push_back(unichar);
++i;
}
- return wout;
+ return { wout.begin(), wout.end() };
}
std::string wstring_to_utf8str(const llwchar* utf32str, size_t len)
{
- std::string out;
+ std::ostringstream out;
- S32 i = 0;
- while (i < len)
+ for (size_t i = 0; i < len; ++i)
{
- char tchars[8]; /* Flawfinder: ignore */
- auto n = wchar_to_utf8chars(utf32str[i], tchars);
- tchars[n] = 0;
- out += tchars;
- i++;
+ out << wchar_to_utf8chars(utf32str[i]);
}
- return out;
+ return out.str();
}
std::string utf16str_to_utf8str(const U16* utf16str, size_t len)
@@ -685,7 +696,21 @@ llwchar utf8str_to_wchar(const std::string& utf8str, size_t offset, size_t lengt
std::string utf8str_showBytesUTF8(const std::string& utf8str)
{
- std::string result;
+ std::ostringstream result;
+ char lastchar = '\0';
+ auto append = [&result, &lastchar](char c)
+ {
+ lastchar = c;
+ result << c;
+ };
+ auto appends = [&result, &lastchar](const std::string& s)
+ {
+ if (! s.empty())
+ {
+ lastchar = s.back();
+ result << s;
+ }
+ };
bool in_sequence = false;
size_t sequence_size = 0;
@@ -694,9 +719,9 @@ std::string utf8str_showBytesUTF8(const std::string& utf8str)
auto open_sequence = [&]()
{
- if (!result.empty() && result.back() != '\n')
- result += '\n'; // Use LF as a separator before new UTF-8 sequence
- result += '[';
+ if (lastchar != '\0' && lastchar != '\n')
+ append('\n'); // Use LF as a separator before new UTF-8 sequence
+ append('[');
in_sequence = true;
};
@@ -705,9 +730,9 @@ std::string utf8str_showBytesUTF8(const std::string& utf8str)
llwchar unicode = utf8str_to_wchar(utf8str, byte_index - sequence_size, sequence_size);
if (unicode != LL_UNKNOWN_CHAR)
{
- result += llformat("+%04X", unicode);
+ appends(llformat("+%04X", unicode));
}
- result += ']';
+ append(']');
in_sequence = false;
sequence_size = 0;
};
@@ -728,9 +753,9 @@ std::string utf8str_showBytesUTF8(const std::string& utf8str)
}
else // Continue the same UTF-8 sequence
{
- result += '.';
+ append('.');
}
- result += llformat("%02X", byte); // The byte is represented in hexadecimal form
+ appends(llformat("%02X", byte)); // The byte is represented in hexadecimal form
++sequence_size;
}
else // ASCII symbol is represented as a character
@@ -740,10 +765,10 @@ std::string utf8str_showBytesUTF8(const std::string& utf8str)
close_sequence();
if (byte != '\n')
{
- result += '\n'; // Use LF as a separator between UTF-8 and ASCII
+ append('\n'); // Use LF as a separator between UTF-8 and ASCII
}
}
- result += byte;
+ append(byte);
}
++byte_index;
}
@@ -753,7 +778,7 @@ std::string utf8str_showBytesUTF8(const std::string& utf8str)
close_sequence();
}
- return result;
+ return result.str();
}
// Search for any emoji symbol, return true if found
@@ -1594,7 +1619,7 @@ S32 LLStringUtil::format(std::string& s, const format_map_t& substitutions)
LL_PROFILE_ZONE_SCOPED_CATEGORY_STRING;
S32 res = 0;
- std::string output;
+ std::ostringstream output;
std::vector<std::string> tokens;
std::string::size_type start = 0;
@@ -1602,7 +1627,7 @@ S32 LLStringUtil::format(std::string& s, const format_map_t& substitutions)
std::string::size_type key_start = 0;
while ((key_start = getSubstitution(s, start, tokens)) != std::string::npos)
{
- output += std::string(s, prev_start, key_start-prev_start);
+ output << std::string(s, prev_start, key_start-prev_start);
prev_start = start;
bool found_replacement = false;
@@ -1643,20 +1668,20 @@ S32 LLStringUtil::format(std::string& s, const format_map_t& substitutions)
if (found_replacement)
{
- output += replacement;
+ output << replacement;
res++;
}
else
{
// we had no replacement, use the string as is
// e.g. "hello [MISSING_REPLACEMENT]" or "-=[Stylized Name]=-"
- output += std::string(s, key_start, start-key_start);
+ output << std::string(s, key_start, start-key_start);
}
tokens.clear();
}
// send the remainder of the string (with no further matches for bracketed names)
- output += std::string(s, start);
- s = output;
+ output << std::string(s, start);
+ s = output.str();
return res;
}
@@ -1672,7 +1697,7 @@ S32 LLStringUtil::format(std::string& s, const LLSD& substitutions)
return res;
}
- std::string output;
+ std::ostringstream output;
std::vector<std::string> tokens;
std::string::size_type start = 0;
@@ -1680,7 +1705,7 @@ S32 LLStringUtil::format(std::string& s, const LLSD& substitutions)
std::string::size_type key_start = 0;
while ((key_start = getSubstitution(s, start, tokens)) != std::string::npos)
{
- output += std::string(s, prev_start, key_start-prev_start);
+ output << std::string(s, prev_start, key_start-prev_start);
prev_start = start;
bool found_replacement = false;
@@ -1713,20 +1738,20 @@ S32 LLStringUtil::format(std::string& s, const LLSD& substitutions)
if (found_replacement)
{
- output += replacement;
+ output << replacement;
res++;
}
else
{
// we had no replacement, use the string as is
// e.g. "hello [MISSING_REPLACEMENT]" or "-=[Stylized Name]=-"
- output += std::string(s, key_start, start-key_start);
+ output << std::string(s, key_start, start-key_start);
}
tokens.clear();
}
// send the remainder of the string (with no further matches for bracketed names)
- output += std::string(s, start);
- s = output;
+ output << std::string(s, start);
+ s = output.str();
return res;
}
diff --git a/indra/llcommon/llstring.h b/indra/llcommon/llstring.h
index db716b1431..b552aede82 100644
--- a/indra/llcommon/llstring.h
+++ b/indra/llcommon/llstring.h
@@ -38,7 +38,9 @@
#include <algorithm>
#include <vector>
#include <map>
+#include <type_traits>
#include "llformat.h"
+#include "stdtypes.h"
#if LL_LINUX
#include <wctype.h>
@@ -313,6 +315,14 @@ public:
static void trim(string_type& string) { trimHead(string); trimTail(string); }
static void truncate(string_type& string, size_type count);
+ // if string startsWith prefix, remove it and return true
+ static bool removePrefix(string_type& string, const string_type& prefix);
+ // if string startsWith prefix, return (string without prefix, true), else (string, false)
+ static std::pair<string_type, bool> withoutPrefix(const string_type& string, const string_type& prefix);
+ // like removePrefix()
+ static bool removeSuffix(string_type& string, const string_type& suffix);
+ static std::pair<string_type, bool> withoutSuffix(const string_type& string, const string_type& suffix);
+
static void toUpper(string_type& string);
static void toLower(string_type& string);
@@ -521,11 +531,38 @@ struct ll_convert_impl
TO operator()(const FROM& in) const;
};
-// Use a function template to get the nice ll_convert<TO>(from_value) API.
+/**
+ * somefunction(ll_convert(data))
+ * target = ll_convert(data)
+ * totype otherfunc(const fromtype& data)
+ * {
+ * // ...
+ * return ll_convert(data);
+ * }
+ * all infer both the FROM type and the TO type.
+ */
+template <typename FROM>
+class ll_convert
+{
+private:
+ const FROM& mRef;
+
+public:
+ ll_convert(const FROM& ref): mRef(ref) {}
+
+ template <typename TO>
+ inline operator TO() const
+ {
+ return ll_convert_impl<TO, std::decay_t<const FROM>>()(mRef);
+ }
+};
+
+// When the TO type must be explicit, use a function template to get
+// ll_convert_to<TO>(from_value) API.
template<typename TO, typename FROM>
-TO ll_convert(const FROM& in)
+TO ll_convert_to(const FROM& in)
{
- return ll_convert_impl<TO, FROM>()(in);
+ return ll_convert_impl<TO, std::decay_t<const FROM>>()(in);
}
// degenerate case
@@ -579,8 +616,8 @@ inline size_t ll_convert_length<char> (const char* zstr) { return std::strl
// and longname(const string&, len) so calls written pre-ll_convert() will
// work. Most of these overloads will be unified once we turn on C++17 and can
// use std::string_view.
-// It also uses aliasmacro to ensure that both ll_convert<OUTSTR>(const char*)
-// and ll_convert<OUTSTR>(const string&) will work.
+// It also uses aliasmacro to ensure that both ll_convert(const char*)
+// and ll_convert(const string&) will work.
#define ll_convert_forms(aliasmacro, OUTSTR, INSTR, longname) \
LL_COMMON_API OUTSTR longname(const INSTR::value_type* in, size_t len); \
inline auto longname(const INSTR& in, size_t len) \
@@ -670,7 +707,11 @@ ll_convert_forms(ll_convert_alias, LLWString, std::string, utf8str_to_
// Same function, better name. JC
inline LLWString utf8string_to_wstring(const std::string& utf8_string) { return utf8str_to_wstring(utf8_string); }
-LL_COMMON_API std::ptrdiff_t wchar_to_utf8chars(llwchar inchar, char* outchars);
+// return a UTF-8 string representation of a single llwchar, which we
+// occasionally require:
+// cheaper than ll_convert_to<std::string>(LLWString(1, inchar))
+LL_COMMON_API std::string wchar_to_utf8chars(llwchar inchar);
+ll_convert_alias(std::string, llwchar, wchar_to_utf8chars(in));
ll_convert_forms(ll_convert_alias, std::string, LLWString, wstring_to_utf8str);
ll_convert_forms(ll_convert_u16_alias, std::string, llutf16string, utf16str_to_utf8str);
@@ -823,7 +864,7 @@ LL_COMMON_API std::string ll_convert_string_to_utf8_string(const std::string& in
template<typename STRING>
STRING windows_message(unsigned long error)
{
- return ll_convert<STRING>(windows_message<std::wstring>(error));
+ return ll_convert(windows_message<std::wstring>(error));
}
/// There's only one real implementation
@@ -1467,6 +1508,60 @@ void LLStringUtilBase<T>::trimTail(string_type& string)
}
}
+// if string startsWith prefix, remove it and return true
+template<class T>
+bool LLStringUtilBase<T>::removePrefix(string_type& string, const string_type& prefix)
+{
+ bool found{ startsWith(string, prefix) };
+ if (found)
+ {
+ string.erase(0, prefix.length());
+ }
+ return found;
+}
+
+// if string startsWith prefix, return (string without prefix, true), else (string, false)
+template<class T>
+std::pair<typename LLStringUtilBase<T>::string_type, bool>
+LLStringUtilBase<T>::withoutPrefix(const string_type& string, const string_type& prefix)
+{
+ bool found{ startsWith(string, prefix) };
+ if (! found)
+ {
+ return { string, false };
+ }
+ else
+ {
+ return { string.substr(prefix.length()), true };
+ }
+}
+
+// like removePrefix()
+template<class T>
+bool LLStringUtilBase<T>::removeSuffix(string_type& string, const string_type& suffix)
+{
+ bool found{ endsWith(string, suffix) };
+ if (found)
+ {
+ string.erase(string.length() - suffix.length());
+ }
+ return found;
+}
+
+template<class T>
+std::pair<typename LLStringUtilBase<T>::string_type, bool>
+LLStringUtilBase<T>::withoutSuffix(const string_type& string, const string_type& suffix)
+{
+ bool found{ endsWith(string, suffix) };
+ if (! found)
+ {
+ return { string, false };
+ }
+ else
+ {
+ return { string.substr(0, string.length() - suffix.length()), true };
+ }
+}
// Replace line feeds with carriage return-line feed pairs.
//static
@@ -1835,7 +1930,7 @@ auto LLStringUtilBase<T>::getoptenv(const std::string& key) -> std::optional<str
if (found)
{
// return populated std::optional
- return { ll_convert<string_type>(*found) };
+ return { ll_convert_to<string_type>(*found) };
}
else
{
diff --git a/indra/llcommon/lockstatic.cpp b/indra/llcommon/lockstatic.cpp
new file mode 100755
index 0000000000..f531ef331e
--- /dev/null
+++ b/indra/llcommon/lockstatic.cpp
@@ -0,0 +1,26 @@
+/**
+ * @file lockstatic.cpp
+ * @author Nat Goodspeed
+ * @date 2024-05-23
+ * @brief Implementation for lockstatic.
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Copyright (c) 2024, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "lockstatic.h"
+// STL headers
+// std headers
+// external library headers
+// other Linden headers
+#include "llerror.h"
+#include "stringize.h"
+
+void llthread::LockStaticBase::throwDead(const char* mangled)
+{
+ LLTHROW(Dead(stringize(LLError::Log::demangle(mangled), " called after cleanup()")));
+}
diff --git a/indra/llcommon/lockstatic.h b/indra/llcommon/lockstatic.h
index 7cc9b7eec0..e83957b1fd 100644
--- a/indra/llcommon/lockstatic.h
+++ b/indra/llcommon/lockstatic.h
@@ -14,21 +14,36 @@
#define LL_LOCKSTATIC_H
#include "mutex.h" // std::unique_lock
+#include "llexception.h"
+#include <typeinfo>
namespace llthread
{
+class LockStaticBase
+{
+public:
+ // trying to lock Static after cleanup() has been called
+ struct Dead: public LLException
+ {
+ Dead(const std::string& what): LLException(what) {}
+ };
+
+protected:
+ static void throwDead(const char* mangled);
+};
+
// Instantiate this template to obtain a pointer to the canonical static
// instance of Static while holding a lock on that instance. Use of
// Static::mMutex presumes that Static declares some suitable mMutex.
template <typename Static>
-class LockStatic
+class LockStatic: public LockStaticBase
{
typedef std::unique_lock<decltype(Static::mMutex)> lock_t;
public:
LockStatic():
mData(getStatic()),
- mLock(mData->mMutex)
+ mLock(getLock(mData))
{}
Static* get() const { return mData; }
operator Static*() const { return get(); }
@@ -40,31 +55,69 @@ public:
mData = nullptr;
mLock.unlock();
}
+ // explicit destruction
+ // We used to store a static instance of Static in getStatic(). The
+ // trouble with that is that at some point during final termination
+ // cleanup, the compiler calls ~Static(), destroying the mutex. If some
+ // later static object's destructor tries to lock our Static, we blow up
+ // trying to lock a destroyed mutex object. This can happen, for instance,
+ // if some class's destructor tries to reference an LLSingleton.
+ // Since a plain dumb pointer has no destructor, the compiler leaves it
+ // alone, so the referenced heap Static instance can survive until we
+ // explicitly call this method.
+ void cleanup()
+ {
+ // certainly don't claim to lock after this point!
+ mData = nullptr;
+ Static*& ptrref{ getStatic() };
+ Static* ptr{ ptrref };
+ ptrref = nullptr;
+ delete ptr;
+ }
protected:
Static* mData;
lock_t mLock;
private:
- Static* getStatic()
+ static lock_t getLock(Static* data)
+ {
+ // data can be false if cleanup() has already been called. If so, no
+ // code in the caller is valid that depends on this instance. We dare
+ // to throw an exception because trying to lock Static after it's been
+ // deleted is not part of normal processing. There are callers who
+ // want to handle this exception, but it should indeed be treated as
+ // exceptional.
+ if (! data)
+ {
+ throwDead(typeid(LockStatic<Static>).name());
+ }
+ // Usual case: data isn't nullptr, carry on.
+ return lock_t(data->mMutex);
+ }
+
+ Static*& getStatic()
{
- // Static::mMutex must be function-local static rather than class-
- // static. Some of our consumers must function properly (therefore
- // lock properly) even when the containing module's static variables
- // have not yet been runtime-initialized. A mutex requires
+ // Our Static instance must be function-local static rather than
+ // class-static. Some of our consumers must function properly
+ // (therefore lock properly) even when the containing module's static
+ // variables have not yet been runtime-initialized. A mutex requires
// construction. A static class member might not yet have been
// constructed.
//
- // We could store a dumb mutex_t*, notice when it's NULL and allocate a
- // heap mutex -- but that's vulnerable to race conditions. And we can't
- // defend the dumb pointer with another mutex.
+ // We could store a dumb mutex_t* class member, notice when it's NULL
+ // and allocate a heap mutex -- but that's vulnerable to race
+ // conditions. And we can't defend the dumb pointer with another
+ // mutex.
//
// We could store a std::atomic<mutex_t*> -- but a default-constructed
// std::atomic<T> does not contain a valid T, even a default-constructed
// T! Which means std::atomic, too, requires runtime initialization.
//
// But a function-local static is guaranteed to be initialized exactly
- // once: the first time control reaches that declaration.
- static Static sData;
- return &sData;
+ // once: the first time control reaches that declaration. Importantly,
+ // since a plain dumb pointer has no destructor, the compiler lets our
+ // heap Static instance survive until someone calls cleanup() (above).
+ static Static* sData{ new Static };
+ return sData;
}
};
diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp
new file mode 100644
index 0000000000..2557fd0cc9
--- /dev/null
+++ b/indra/llcommon/lua_function.cpp
@@ -0,0 +1,1670 @@
+/**
+ * @file lua_function.cpp
+ * @author Nat Goodspeed
+ * @date 2024-02-05
+ * @brief Implementation for lua_function.
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Copyright (c) 2024, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "lua_function.h"
+// STL headers
+// std headers
+#include <algorithm>
+#include <exception>
+#include <iomanip> // std::quoted
+#include <map>
+#include <memory> // std::unique_ptr
+#include <typeinfo>
+#include <unordered_map>
+// external library headers
+// other Linden headers
+#include "commoncontrol.h"
+#include "fsyspath.h"
+#include "hexdump.h"
+#include "llcoros.h"
+#include "lleventcoro.h"
+#include "llsd.h"
+#include "llsdutil.h"
+#include "llstring.h"
+#include "lualistener.h"
+#include "stringize.h"
+
+using namespace std::literals; // e.g. std::string_view literals: "this"sv
+
+const S32 INTERRUPTS_MAX_LIMIT = 100000;
+const S32 INTERRUPTS_SUSPEND_LIMIT = 100;
+
+#define lua_register(L, n, f) (lua_pushcfunction(L, (f), n), lua_setglobal(L, (n)))
+#define lua_rawlen lua_objlen
+
+int DistinctInt::mValues{0};
+
+/*****************************************************************************
+* lluau namespace
+*****************************************************************************/
+namespace
+{
+ // can't specify free function free() as a unique_ptr deleter
+ struct freer
+ {
+ void operator()(void* ptr){ free(ptr); }
+ };
+} // anonymous namespace
+
+namespace lluau
+{
+
+int dostring(lua_State* L, const std::string& desc, const std::string& text)
+{
+ auto r = loadstring(L, desc, text);
+ if (r != LUA_OK)
+ return r;
+
+ // Push debug.traceback() onto the stack as lua_pcall()'s error
+ // handler function. On error, lua_pcall() calls the specified error
+ // handler function with the original error message; the message
+ // returned by the error handler is then returned by lua_pcall().
+ // Luau's debug.traceback() is called with a message to prepend to the
+ // returned traceback string. Almost as if they'd been designed to
+ // work together...
+ lua_getglobal(L, "debug");
+ lua_getfield(L, -1, "traceback");
+ // ditch "debug"
+ lua_remove(L, -2);
+ // stack: compiled chunk, debug.traceback()
+ lua_insert(L, -2);
+ // stack: debug.traceback(), compiled chunk
+ LuaRemover cleanup(L, -2);
+
+ // It's important to pass LUA_MULTRET as the expected number of return
+ // values: if we pass any fixed number, we discard any returned values
+ // beyond that number.
+ return lua_pcall(L, 0, LUA_MULTRET, -2);
+}
+
+int loadstring(lua_State *L, const std::string &desc, const std::string &text)
+{
+ size_t bytecodeSize = 0;
+ // The char* returned by luau_compile() must be freed by calling free().
+ // Use unique_ptr so the memory will be freed even if luau_load() throws.
+ std::unique_ptr<char[], freer> bytecode{
+ luau_compile(text.data(), text.length(), nullptr, &bytecodeSize)};
+ return luau_load(L, desc.data(), bytecode.get(), bytecodeSize, 0);
+}
+
+fsyspath source_path(lua_State* L)
+{
+ //Luau lua_Debug and lua_getinfo() are different compared to default Lua:
+ //see https://github.com/luau-lang/luau/blob/80928acb92d1e4b6db16bada6d21b1fb6fa66265/VM/include/lua.h
+ // In particular:
+ // passing level=1 gets you info about the deepest function call
+ // passing level=lua_stackdepth() gets you info about the topmost script
+ // Empirically, lua_getinfo(level > 1) behaves strangely (including
+ // crashing the program) unless you iterate from 1 to desired level.
+ lua_Debug ar{};
+ for (int i(0), depth(lua_stackdepth(L)); i <= depth; ++i)
+ {
+ lua_getinfo(L, i, "s", &ar);
+ }
+ return ar.source;
+}
+
+} // namespace lluau
+
+/*****************************************************************************
+* lua_destroyuserdata(), lua_destroybounduserdata() (see lua_emplace<T>())
+*****************************************************************************/
+int lua_destroyuserdata(lua_State* L)
+{
+ // stack: lua_emplace() userdata to be destroyed
+ if (int tag;
+ lua_isuserdata(L, -1) &&
+ (tag = lua_userdatatag(L, -1)) != 0)
+ {
+ auto dtor = lua_getuserdatadtor(L, tag);
+ // detach this userdata from the destructor with tag 'tag'
+ lua_setuserdatatag(L, -1, 0);
+ // now run the real destructor
+ dtor(L, lua_touserdata(L, -1));
+ }
+ lua_settop(L, 0);
+ return 0;
+}
+
+int lua_destroybounduserdata(lua_State *L)
+{
+ // called with no arguments -- push bound upvalue
+ lluau_checkstack(L, 1);
+ lua_pushvalue(L, lua_upvalueindex(1));
+ return lua_destroyuserdata(L);
+}
+
+/*****************************************************************************
+* Lua <=> C++ conversions
+*****************************************************************************/
+std::string lua_tostdstring(lua_State* L, int index)
+{
+ lua_checkdelta(L);
+ size_t len;
+ const char* strval{ lua_tolstring(L, index, &len) };
+ return { strval, len };
+}
+
+void lua_pushstdstring(lua_State* L, const std::string& str)
+{
+ lua_checkdelta(L, 1);
+ lluau_checkstack(L, 1);
+ lua_pushlstring(L, str.c_str(), str.length());
+}
+
+// By analogy with existing lua_tomumble() functions, return an LLSD object
+// corresponding to the Lua object at stack index 'index' in state L.
+// This function assumes that a Lua caller is fully aware that they're trying
+// to call a viewer function. In other words, the caller must specifically
+// construct Lua data convertible to LLSD.
+//
+// For proper error handling, we REQUIRE that the Lua runtime be compiled as
+// C++ so errors are raised as C++ exceptions rather than as longjmp() calls:
+// http://www.lua.org/manual/5.4/manual.html#4.4
+// "Internally, Lua uses the C longjmp facility to handle errors. (Lua will
+// use exceptions if you compile it as C++; search for LUAI_THROW in the
+// source code for details.)"
+// Some blocks within this function construct temporary C++ objects in the
+// expectation that these objects will be properly destroyed even if code
+// reached by that block raises a Lua error.
+LLSD lua_tollsd(lua_State* L, int index)
+{
+ lua_checkdelta(L);
+ switch (lua_type(L, index))
+ {
+ case LUA_TNONE:
+ // Should LUA_TNONE be an error instead of returning isUndefined()?
+ case LUA_TNIL:
+ return {};
+
+ case LUA_TBOOLEAN:
+ return bool(lua_toboolean(L, index));
+
+ case LUA_TNUMBER:
+ {
+ // Vanilla Lua supports lua_tointegerx(), which tells the caller
+ // whether the number at the specified stack index is or is not an
+ // integer. Apparently the function exists but does not work right in
+ // Luau: it reports even non-integer numbers as integers.
+ // Instead, check if integer truncation leaves the number intact.
+ lua_Number numval{ lua_tonumber(L, index) };
+ lua_Integer intval{ narrow(numval) };
+ if (lua_Number(intval) == numval)
+ {
+ return LLSD::Integer(intval);
+ }
+ else
+ {
+ return numval;
+ }
+ }
+
+ case LUA_TSTRING:
+ return lua_tostdstring(L, index);
+
+ case LUA_TUSERDATA:
+ {
+ LLSD::Binary binary(lua_rawlen(L, index));
+ std::memcpy(binary.data(), lua_touserdata(L, index), binary.size());
+ return binary;
+ }
+
+ case LUA_TTABLE:
+ {
+ // A Lua table correctly constructed to convert to LLSD will have
+ // either consecutive integer keys starting at 1, which we represent
+ // as an LLSD array (with Lua key 1 at C++ index 0), or will have
+ // all string keys.
+ //
+ // In the belief that Lua table traversal skips "holes," that is, it
+ // doesn't report any key/value pair whose value is nil, we allow a
+ // table with integer keys >= 1 but with "holes." This produces an
+ // LLSD array with isUndefined() entries at unspecified keys. There
+ // would be no other way for a Lua caller to construct an
+ // isUndefined() LLSD array entry. However, to guard against crazy int
+ // keys, we forbid gaps larger than a certain size: crazy int keys
+ // could result in a crazy large contiguous LLSD array.
+ //
+ // Possible looseness could include:
+ // - A mix of integer and string keys could produce an LLSD map in
+ // which the integer keys are converted to string. (Key conversion
+ // must be performed in C++, not Lua, to avoid confusing
+ // lua_next().)
+ // - However, since in Lua t[0] and t["0"] are distinct table entries,
+ // do not consider converting numeric string keys to int to return
+ // an LLSD array.
+ // But until we get more experience with actual Lua scripts in
+ // practice, let's say that any deviation is a Lua coding error.
+ // An important property of the strict definition above is that most
+ // conforming data blobs can make a round trip across the language
+ // boundary and still compare equal. A non-conforming data blob would
+ // lose that property.
+ // Known exceptions to round trip identity:
+ // - Empty LLSD map and empty LLSD array convert to empty Lua table.
+ // But empty Lua table converts to isUndefined() LLSD object.
+ // - LLSD::Real with integer value returns as LLSD::Integer.
+ // - LLSD::UUID, LLSD::Date and LLSD::URI all convert to Lua string,
+ // and so return as LLSD::String.
+ // - Lua does not store any table key whose value is nil. An LLSD
+ // array with isUndefined() entries produces a Lua table with
+ // "holes" in the int key sequence; this converts back to an LLSD
+ // array containing corresponding isUndefined() entries -- except
+ // when one or more of the final entries isUndefined(). These are
+ // simply dropped, producing a shorter LLSD array than the original.
+ // - For the same reason, any keys in an LLSD map whose value
+ // isUndefined() are simply discarded in the converted Lua table.
+ // This converts back to an LLSD map lacking those keys.
+ // - If it's important to preserve the original length of an LLSD
+ // array whose final entries are undefined, or the full set of keys
+ // for an LLSD map some of whose values are undefined, store an
+ // LLSD::emptyArray() or emptyMap() instead. These will be
+ // represented in Lua as empty table, which should convert back to
+ // undefined LLSD. Naturally, though, those won't survive a second
+ // round trip.
+
+ // This is the most important of the lluau_checkstack() calls because a
+ // deeply nested Lua structure will enter this case at each level, and
+ // we'll need another 2 stack slots to traverse each nested table.
+ lluau_checkstack(L, 2);
+ // BEFORE we push nil to initialize the lua_next() traversal, convert
+ // 'index' to absolute! Our caller might have passed a relative index;
+ // we do, below: lua_tollsd(L, -1). If 'index' is -1, then when we
+ // push nil, what we find at index -1 is nil, not the table!
+ index = lua_absindex(L, index);
+ lua_pushnil(L); // first key
+ if (! lua_next(L, index))
+ {
+ // it's a table, but the table is empty -- no idea if it should be
+ // modeled as empty array or empty map -- return isUndefined(),
+ // which can be consumed as either
+ return {};
+ }
+ // key is at stack index -2, value at index -1
+ // from here until lua_next() returns 0, have to lua_pop(2) if we
+ // return early
+ LuaPopper popper(L, 2);
+ // Remember the type of the first key
+ auto firstkeytype{ lua_type(L, -2) };
+ switch (firstkeytype)
+ {
+ case LUA_TNUMBER:
+ {
+ // First Lua key is a number: try to convert table to LLSD array.
+ // This is tricky because we don't know in advance the size of the
+ // array. The Lua reference manual says that lua_rawlen() is the
+ // same as the length operator '#'; but the length operator states
+ // that it might stop at any "hole" in the subject table.
+ // Moreover, the Lua next() function (and presumably lua_next())
+ // traverses a table in unspecified order, even for numeric keys
+ // (emphasized in the doc).
+ // Make a preliminary pass over the whole table to validate and to
+ // collect keys.
+ std::vector<LLSD::Integer> keys;
+ // Try to determine the length of the table. If the length
+ // operator is truthful, avoid allocations while we grow the keys
+ // vector. Even if it's not, we can still grow the vector, albeit
+ // a little less efficiently.
+ keys.reserve(lua_objlen(L, index));
+ do
+ {
+ auto arraykeytype{ lua_type(L, -2) };
+ switch (arraykeytype)
+ {
+ case LUA_TNUMBER:
+ {
+ int isint;
+ lua_Integer intkey{ lua_tointegerx(L, -2, &isint) };
+ if (! isint)
+ {
+ // key isn't an integer - this doesn't fit our LLSD
+ // array constraints
+ return lluau::error(L, "Expected integer array key, got %f instead",
+ lua_tonumber(L, -2));
+ }
+ if (intkey < 1)
+ {
+ return lluau::error(L, "array key %d out of bounds", int(intkey));
+ }
+
+ keys.push_back(LLSD::Integer(intkey));
+ break;
+ }
+
+ case LUA_TSTRING:
+ // break out strings specially to report the value
+ return lluau::error(L, "Cannot convert string array key '%s' to LLSD",
+ lua_tostring(L, -2));
+
+ default:
+ return lluau::error(L, "Cannot convert %s array key to LLSD",
+ lua_typename(L, arraykeytype));
+ }
+
+ // remove value, keep key for next iteration
+ lua_pop(L, 1);
+ } while (lua_next(L, index) != 0);
+ popper.disarm();
+ // Table keys are all integers: are they reasonable integers?
+ // Arbitrary max: may bite us, but more likely to protect us
+ const size_t array_max{ 10000 };
+ if (keys.size() > array_max)
+ {
+ return lluau::error(L, "Conversion from Lua to LLSD array limited to %d entries",
+ int(array_max));
+ }
+ // We know the smallest key is >= 1. Check the largest. We also
+ // know the vector is NOT empty, else we wouldn't have gotten here.
+ std::sort(keys.begin(), keys.end());
+ LLSD::Integer highkey = *keys.rbegin();
+ if ((highkey - LLSD::Integer(keys.size())) > 100)
+ {
+ // Looks like we've gone beyond intentional array gaps into
+ // crazy key territory.
+ return lluau::error(L, "Gaps in Lua table too large for conversion to LLSD array");
+ }
+ // right away expand the result array to the size we'll need
+ LLSD result{ LLSD::emptyArray() };
+ result[highkey - 1] = LLSD();
+ // Traverse the table again, and this time populate result array.
+ lua_pushnil(L); // first key
+ while (lua_next(L, index))
+ {
+ // key at stack index -2, value at index -1
+ // We've already validated lua_tointegerx() for each key.
+ auto key{ lua_tointeger(L, -2) };
+ // Don't forget to subtract 1 from Lua key for LLSD subscript!
+ result[LLSD::Integer(key) - 1] = lua_tollsd(L, -1);
+ // remove value, keep key for next iteration
+ lua_pop(L, 1);
+ }
+ return result;
+ }
+
+ case LUA_TSTRING:
+ {
+ // First Lua key is a string: try to convert table to LLSD map
+ LLSD result{ LLSD::emptyMap() };
+ do
+ {
+ auto mapkeytype{ lua_type(L, -2) };
+ if (mapkeytype != LUA_TSTRING)
+ {
+ return lluau::error(L, "Cannot convert %s map key to LLSD",
+ lua_typename(L, mapkeytype));
+ }
+
+ auto key{ lua_tostdstring(L, -2) };
+ result[key] = lua_tollsd(L, -1);
+ // remove value, keep key for next iteration
+ lua_pop(L, 1);
+ } while (lua_next(L, index) != 0);
+ popper.disarm();
+ return result;
+ }
+
+ default:
+ // First Lua key isn't number or string: sorry
+ return lluau::error(L, "Cannot convert %s table key to LLSD",
+ lua_typename(L, firstkeytype));
+ }
+ }
+
+ default:
+ // Other Lua entities (e.g. function, C function, light userdata,
+ // thread, userdata) are not convertible to LLSD, indicating a coding
+ // error in the caller.
+ return lluau::error(L, "Cannot convert type %s to LLSD", luaL_typename(L, index));
+ }
+}
+
+// By analogy with existing lua_pushmumble() functions, push onto state L's
+// stack a Lua object corresponding to the passed LLSD object.
+void lua_pushllsd(lua_State* L, const LLSD& data)
+{
+ lua_checkdelta(L, 1);
+ // might need 2 slots for array or map
+ lluau_checkstack(L, 2);
+ switch (data.type())
+ {
+ case LLSD::TypeUndefined:
+ lua_pushnil(L);
+ break;
+
+ case LLSD::TypeBoolean:
+ lua_pushboolean(L, data.asBoolean());
+ break;
+
+ case LLSD::TypeInteger:
+ lua_pushinteger(L, data.asInteger());
+ break;
+
+ case LLSD::TypeReal:
+ lua_pushnumber(L, data.asReal());
+ break;
+
+ case LLSD::TypeBinary:
+ {
+ auto binary{ data.asBinary() };
+ std::memcpy(lua_newuserdata(L, binary.size()),
+ binary.data(), binary.size());
+ break;
+ }
+
+ case LLSD::TypeMap:
+ {
+ // push a new table with space for our non-array keys
+ lua_createtable(L, 0, narrow(data.size()));
+ for (const auto& pair: llsd::inMap(data))
+ {
+ // push value -- so now table is at -2, value at -1
+ lua_pushllsd(L, pair.second);
+ // pop value, assign to table[key]
+ lua_setfield(L, -2, pair.first.c_str());
+ }
+ break;
+ }
+
+ case LLSD::TypeArray:
+ {
+ // push a new table with space for array entries
+ lua_createtable(L, narrow(data.size()), 0);
+ lua_Integer key{ 0 };
+ for (const auto& item: llsd::inArray(data))
+ {
+ // push new array value: table at -2, value at -1
+ lua_pushllsd(L, item);
+ // pop value, assign table[key] = value
+ lua_rawseti(L, -2, ++key);
+ }
+ break;
+ }
+
+ case LLSD::TypeString:
+ case LLSD::TypeUUID:
+ case LLSD::TypeDate:
+ case LLSD::TypeURI:
+ default:
+ {
+ lua_pushstdstring(L, data.asString());
+ break;
+ }
+ }
+}
+
+/*****************************************************************************
+* LuaState class
+*****************************************************************************/
+namespace
+{
+
+// If we find we're running Lua scripts from more than one thread, sLuaStateMap
+// should be thread_local. Until then, avoid the overhead.
+using LuaStateMap = std::unordered_map<lua_State*, LuaState*>;
+static LuaStateMap sLuaStateMap;
+
+// replace table-at-index[name] with passed func,
+// binding the original table-at-index[name] as func's upvalue
+void replace_entry(lua_State* L, int index,
+ const std::string& name, lua_CFunction func);
+
+// replacement next() function that understands setdtor() proxy args
+int lua_proxydrill(lua_State* L);
+// replacement pairs() function that supports __iter() metamethod
+int lua_metapairs(lua_State* L);
+// replacement ipairs() function that supports __index() metamethod
+int lua_metaipairs(lua_State* L);
+// helper for lua_metaipairs() (actual generator function)
+int lua_metaipair(lua_State* L);
+
+} // anonymous namespace
+
+LuaState::LuaState(script_finished_fn cb):
+ mCallback(cb)
+{
+ /*---------------------------- feature flag ----------------------------*/
+ try
+ {
+ mFeature = LL::CommonControl::get("Global", "LuaFeature").asBoolean();
+ }
+ catch (const LL::CommonControl::NoListener&)
+ {
+ // If this program doesn't have an LLViewerControlListener,
+ // it's probably a test program; go ahead.
+ mFeature = true;
+ }
+ catch (const LL::CommonControl::ParamError&)
+ {
+ // We found LLViewerControlListener, but its settings do not include
+ // "LuaFeature". Hmm, fishy: that feature flag was introduced at the
+ // same time as this code.
+ mFeature = false;
+ }
+ // None of the rest of this is necessary if we're not going to run anything.
+ if (! mFeature)
+ {
+ mError = "Lua feature disabled";
+ return;
+ }
+ /*---------------------------- feature flag ----------------------------*/
+
+ mState = luaL_newstate();
+ // Ensure that we can always find this LuaState instance, given the
+ // lua_State we just created or any of its coroutines.
+ sLuaStateMap.emplace(mState, this);
+ luaL_openlibs(mState);
+ // publish to this new lua_State all the LL entry points we defined using
+ // the lua_function() macro
+ LuaFunction::init(mState);
+ // Try to make print() write to our log.
+ lua_register(mState, "print", LuaFunction::get("print_info"));
+ // We don't want to have to prefix require().
+ lua_register(mState, "require", LuaFunction::get("require"));
+
+ // Replace certain key global functions so they understand our
+ // LL.setdtor() proxy objects.
+ // (We could also do this for selected library functions as well,
+ // e.g. the table, string, math libraries... let's see if needed.)
+ replace_entry(mState, LUA_GLOBALSINDEX, "next", lua_proxydrill);
+ // Replacing pairs() with lua_metapairs() makes global pairs() honor
+ // objects with __iter() metamethods.
+ replace_entry(mState, LUA_GLOBALSINDEX, "pairs", lua_metapairs);
+ // Replacing ipairs() with lua_metaipairs() makes global ipairs() honor
+ // objects with __index() metamethods -- as long as the object in question
+ // has no array entries (int keys) of its own. (If it does, object[i] will
+ // retrieve table[i] instead of calling __index(table, i).)
+ replace_entry(mState, LUA_GLOBALSINDEX, "ipairs", lua_metaipairs);
+}
+
+namespace
+{
+
+void replace_entry(lua_State* L, int index,
+ const std::string& name, lua_CFunction func)
+{
+ index = lua_absindex(L, index);
+ lua_checkdelta(L);
+ // push the function's name string twice
+ lua_pushlstring(L, name.data(), name.length());
+ lua_pushvalue(L, -1);
+ // stack: name, name
+ // look up the existing table entry
+ lua_rawget(L, index);
+ // stack: name, original function
+ // bind original function as the upvalue for func()
+ lua_pushcclosure(L, func, (name + "()").c_str(), 1);
+ // stack: name, func-with-bound-original
+ // table[name] = func-with-bound-original
+ lua_rawset(L, index);
+}
+
+int lua_metapairs(lua_State* L)
+{
+// LuaLog debug(L, "lua_metapairs()");
+ // pairs(obj): object is at index 1
+ // How many args were we passed?
+ int args = lua_gettop(L);
+ // stack: obj, ...
+ if (luaL_getmetafield(L, 1, "__iter"))
+ {
+ // stack: obj, ..., getmetatable(obj).__iter
+ }
+ else
+ {
+ // Push the original pairs() function, captured as our upvalue.
+ lua_pushvalue(L, lua_upvalueindex(1));
+ // stack: obj, ..., original pairs()
+ }
+ lua_insert(L, 1);
+ // stack: (__iter() or pairs()), obj, ...
+ // call whichever function(obj, ...) (args args, up to 3 return values)
+ lua_call(L, args, LUA_MULTRET);
+ // return as many values as the selected function returned
+ return lua_gettop(L);
+}
+
+int lua_metaipairs(lua_State* L)
+{
+// LuaLog debug(L, "lua_metaipairs()");
+ // ipairs(obj): object is at index 1
+ // How many args were we passed?
+ int args = lua_gettop(L);
+ // stack: obj, ...
+ if (luaL_getmetafield(L, 1, "__index"))
+ {
+ // stack: obj, ..., getmetatable(obj).__index
+ // discard __index and everything but obj:
+ // we don't want to call __index(), just check its presence
+ lua_settop(L, 1);
+ // stack: obj
+ lua_pushcfunction(L, lua_metaipair, "lua_metaipair");
+ // stack: obj, lua_metaipair
+ lua_insert(L, 1);
+ // stack: lua_metaipair, obj
+ // push explicit 0 so lua_metaipair need not special-case nil
+ lua_pushinteger(L, 0);
+ // stack: lua_metaipair, obj, 0
+ return 3;
+ }
+ else // no __index() metamethod
+ {
+ // Although our lua_metaipair() function demonstrably works whether or
+ // not our object has an __index() metamethod, the code below assumes
+ // that the Lua engine may have a more efficient implementation for
+ // built-in ipairs() than our lua_metaipair().
+ // Push the original ipairs() function, captured as our upvalue.
+ lua_pushvalue(L, lua_upvalueindex(1));
+ // stack: obj, ..., original ipairs()
+ // Shift the stack so the original function is first.
+ lua_insert(L, 1);
+ // stack: original ipairs(), obj, ...
+ // Call original ipairs() with all original args, no error checking.
+ // Don't truncate however many values that function returns.
+ lua_call(L, args, LUA_MULTRET);
+ // Return as many values as the original function returned.
+ return lua_gettop(L);
+ }
+}
+
+int lua_metaipair(lua_State* L)
+{
+// LuaLog debug(L, "lua_metaipair()");
+ // called with (obj, previous-index)
+ // increment previous-index for this call
+ lua_Integer i = luaL_optinteger(L, 2, 0) + 1;
+ lua_pop(L, 1);
+ // stack: obj
+ lua_pushinteger(L, i);
+ // stack: obj, i
+ lua_pushvalue(L, -1);
+ // stack: obj, i, i
+ lua_insert(L, 1);
+ // stack: i, obj, i
+ lua_gettable(L, -2);
+ // stack: i, obj, obj[i] (honoring __index())
+ lua_remove(L, -2);
+ // stack: i, obj[i]
+ if (! lua_isnil(L, -1))
+ {
+ // great, obj[i] isn't nil: return (i, obj[i])
+ return 2;
+ }
+ // obj[i] is nil. ipairs() is documented to stop at the first hole,
+ // regardless of #obj. Clear the stack, i.e. return nil.
+ lua_settop(L, 0);
+ return 0;
+}
+
+} // anonymous namespace
+
+LuaState::~LuaState()
+{
+ // If we're unwinding the stack due to an exception, don't bother trying
+ // to call any callbacks -- either Lua or C++.
+ if (std::uncaught_exceptions() != 0)
+ return;
+
+ /*---------------------------- feature flag ----------------------------*/
+ if (mFeature)
+ /*---------------------------- feature flag ----------------------------*/
+ {
+ // We're just about to destroy this lua_State mState. Did this Lua chunk
+ // register any atexit() functions?
+ lluau_checkstack(mState, 3);
+ // look up Registry.atexit
+ lua_getfield(mState, LUA_REGISTRYINDEX, "atexit");
+ // stack contains Registry.atexit
+ if (lua_istable(mState, -1))
+ {
+ // We happen to know that Registry.atexit is built by appending array
+ // entries using table.insert(). That's important because it means
+ // there are no holes, and therefore lua_objlen() should be correct.
+ // That's important because we walk the atexit table backwards, to
+ // destroy last the things we created (passed to LL.atexit()) first.
+ int len(lua_objlen(mState, -1));
+ LL_DEBUGS("Lua") << LLCoros::getName() << ": Registry.atexit is a table with "
+ << len << " entries" << LL_ENDL;
+
+ // Push debug.traceback() onto the stack as lua_pcall()'s error
+ // handler function. On error, lua_pcall() calls the specified error
+ // handler function with the original error message; the message
+ // returned by the error handler is then returned by lua_pcall().
+ // Luau's debug.traceback() is called with a message to prepend to the
+ // returned traceback string. Almost as if they'd been designed to
+ // work together...
+ lua_getglobal(mState, "debug");
+ lua_getfield(mState, -1, "traceback");
+ // ditch "debug"
+ lua_remove(mState, -2);
+ // stack now contains atexit, debug.traceback()
+
+ for (int i(len); i >= 1; --i)
+ {
+ lua_pushinteger(mState, i);
+ // stack contains Registry.atexit, debug.traceback(), i
+ lua_gettable(mState, -3);
+ // stack contains Registry.atexit, debug.traceback(), atexit[i]
+ // Call atexit[i](), no args, no return values.
+ // Use lua_pcall() because errors in any one atexit() function
+ // shouldn't cancel the rest of them. Pass debug.traceback() as
+ // the error handler function.
+ LL_DEBUGS("Lua") << LLCoros::getName()
+ << ": calling atexit(" << i << ")" << LL_ENDL;
+ if (lua_pcall(mState, 0, 0, -2) != LUA_OK)
+ {
+ auto error{ lua_tostdstring(mState, -1) };
+ LL_WARNS("Lua") << LLCoros::getName()
+ << ": atexit(" << i << ") error: " << error << LL_ENDL;
+ // pop error message
+ lua_pop(mState, 1);
+ }
+ LL_DEBUGS("Lua") << LLCoros::getName() << ": atexit(" << i << ") done" << LL_ENDL;
+ // lua_pcall() has already popped atexit[i]:
+ // stack contains atexit, debug.traceback()
+ }
+ // pop debug.traceback()
+ lua_pop(mState, 1);
+ }
+ // pop Registry.atexit (either table or nil)
+ lua_pop(mState, 1);
+
+ // with the demise of this LuaState, remove sLuaStateMap entry
+ sLuaStateMap.erase(mState);
+
+ lua_close(mState);
+ }
+
+ if (mCallback)
+ {
+ // mError potentially set by previous checkLua() call(s)
+ mCallback(mError);
+ }
+}
+
+bool LuaState::checkLua(const std::string& desc, int r)
+{
+ if (r != LUA_OK)
+ {
+ mError = lua_tostring(mState, -1);
+ lua_pop(mState, 1);
+
+ LL_WARNS("Lua") << desc << ": " << mError << LL_ENDL;
+ return false;
+ }
+ return true;
+}
+
+std::pair<int, LLSD> LuaState::expr(const std::string& desc, const std::string& text)
+{
+ /*---------------------------- feature flag ----------------------------*/
+ if (! mFeature)
+ {
+ // fake an error
+ return { -1, stringize("Not running ", desc) };
+ }
+ /*---------------------------- feature flag ----------------------------*/
+
+ set_interrupts_counter(0);
+
+ lua_callbacks(mState)->interrupt = [](lua_State *L, int gc)
+ {
+ // skip if we're interrupting only for garbage collection
+ if (gc >= 0)
+ return;
+
+ LLCoros::checkStop();
+ LuaState::getParent(L).check_interrupts_counter();
+ };
+
+ LL_INFOS("Lua") << desc << " run" << LL_ENDL;
+ if (! checkLua(desc, lluau::dostring(mState, desc, text)))
+ {
+ LL_WARNS("Lua") << desc << " error: " << mError << LL_ENDL;
+ return { -1, mError };
+ }
+
+ // here we believe there was no error -- did the Lua fragment leave
+ // anything on the stack?
+ std::pair<int, LLSD> result{ lua_gettop(mState), {} };
+ LL_INFOS("Lua") << desc << " done, " << result.first << " results." << LL_ENDL;
+ if (result.first)
+ {
+ // aha, at least one entry on the stack!
+ if (result.first == 1)
+ {
+ // Don't forget that lua_tollsd() can throw Lua errors.
+ try
+ {
+ result.second = lua_tollsd(mState, 1);
+ }
+ catch (const std::exception& error)
+ {
+ LL_WARNS("Lua") << desc << " error converting result: " << error.what() << LL_ENDL;
+ // lua_tollsd() is designed to be called from a lua_function(),
+ // that is, from a C++ function called by Lua. In case of error,
+ // it throws a Lua error to be caught by the Lua runtime. expr()
+ // is a peculiar use case in which our C++ code is calling
+ // lua_tollsd() after return from the Lua runtime. We must catch
+ // the exception thrown for a Lua error, else it will propagate
+ // out to the main coroutine and terminate the viewer -- but since
+ // we instead of the Lua runtime catch it, our lua_State retains
+ // its internal error status. Any subsequent lua_pcall() calls
+ // with this lua_State will report error regardless of whether the
+ // chunk runs successfully.
+ return { -1, stringize(LLError::Log::classname(error), ": ", error.what()) };
+ }
+ }
+ else
+ {
+ // multiple entries on the stack
+ int index;
+ try
+ {
+ for (index = 1; index <= result.first; ++index)
+ {
+ result.second.append(lua_tollsd(mState, index));
+ }
+ }
+ catch (const std::exception& error)
+ {
+ LL_WARNS("Lua") << desc << " error converting result " << index << ": "
+ << error.what() << LL_ENDL;
+ // see above comments regarding lua_State's error status
+ return { -1, stringize(LLError::Log::classname(error), ": ", error.what()) };
+ }
+ }
+ }
+ // pop everything
+ lua_settop(mState, 0);
+ return result;
+}
+
+// We think we don't need mFeature tests in the rest of these LuaState methods
+// because, if expr() isn't running code, nobody should be calling any of them.
+
+LuaListener& LuaState::obtainListener(lua_State* L)
+{
+ lluau_checkstack(L, 2);
+ lua_getfield(L, LUA_REGISTRYINDEX, "LuaListener");
+ // compare lua_type() because lua_isuserdata() also accepts light userdata
+ if (lua_type(L, -1) != LUA_TUSERDATA)
+ {
+ llassert(lua_type(L, -1) == LUA_TNIL);
+ lua_pop(L, 1);
+ // push a userdata containing new LuaListener, binding L
+ lua_emplace<LuaListener>(L, L);
+ // duplicate the top stack entry so we can store one copy
+ lua_pushvalue(L, -1);
+ lua_setfield(L, LUA_REGISTRYINDEX, "LuaListener");
+ }
+ // At this point, one way or the other, the stack top should be (a Lua
+ // userdata containing) our LuaListener.
+ LuaListener* listener{ lua_toclass<LuaListener>(L, -1) };
+ // Since our LuaListener instance is stored in the Registry, it won't be
+ // garbage collected: it will be destroyed only when lua_close() clears
+ // out the Registry. That's why we dare pop the userdata value off the
+ // stack while still depending on a pointer into its data.
+ lua_pop(L, 1);
+ return *listener;
+}
+
+LuaState& LuaState::getParent(lua_State* L)
+{
+ // Look up the LuaState instance associated with the *script*, not the
+ // specific Lua *coroutine*. In other words, first find this lua_State's
+ // main thread.
+ auto found{ sLuaStateMap.find(lua_mainthread(L)) };
+ // Our constructor creates the map entry, our destructor deletes it. As
+ // long as the LuaState exists, we should be able to find it. And we
+ // SHOULD only be talking to a lua_State managed by a LuaState instance.
+ llassert(found != sLuaStateMap.end());
+ return *found->second;
+}
+
+void LuaState::set_interrupts_counter(S32 counter)
+{
+ mInterrupts = counter;
+}
+
+void LuaState::check_interrupts_counter()
+{
+ // The official way to manage data associated with a lua_State is to store
+ // it *as* Lua data within the lua_State. But this method is called by the
+ // Lua engine via lua_callbacks(L)->interrupt, and empirically we've hit
+ // mysterious Lua data stack overflows trying to use stack-based Lua data
+ // access functions in that situation. It seems the Lua engine is capable
+ // of interrupting itself at a moment when re-entry is not valid. So only
+ // touch data in this LuaState.
+ ++mInterrupts;
+ if (mInterrupts > INTERRUPTS_MAX_LIMIT)
+ {
+ lluau::error(mState, "Possible infinite loop, terminated.");
+ }
+ else if (mInterrupts % INTERRUPTS_SUSPEND_LIMIT == 0)
+ {
+ LL_DEBUGS("Lua") << LLCoros::getName() << " suspending at " << mInterrupts
+ << " interrupts" << LL_ENDL;
+ llcoro::suspend();
+ }
+}
+
+/*****************************************************************************
+* atexit()
+*****************************************************************************/
+lua_function(atexit, "atexit(function): "
+ "register Lua function to be called at script termination")
+{
+ lua_checkdelta(L, -1);
+ lluau_checkstack(L, 4);
+ // look up the global name "table"
+ lua_getglobal(L, "table");
+ // stack contains function, table
+ // look up table.insert
+ lua_getfield(L, -1, "insert");
+ // stack contains function, table, table.insert
+ // ditch table
+ lua_replace(L, -2);
+ // stack contains function, table.insert
+ // find or create the "atexit" table in the Registry
+ luaL_newmetatable(L, "atexit");
+ // stack contains function, table.insert, Registry.atexit
+ // we were called with a Lua function to append to that Registry.atexit
+ // table -- push function
+ lua_pushvalue(L, 1); // or -3
+ // stack contains function, table.insert, Registry.atexit, function
+ // call table.insert(Registry.atexit, function)
+ // don't use pcall(): if there's an error, let it propagate
+ lua_call(L, 2, 0);
+ // stack contains function -- pop everything
+ lua_settop(L, 0);
+ return 0;
+}
+
+/*****************************************************************************
+* LuaPopper class
+*****************************************************************************/
+LuaPopper::~LuaPopper()
+{
+ // If we're unwinding the C++ stack due to an exception, don't pop!
+ if (std::uncaught_exceptions() == 0 && mCount)
+ {
+ lua_pop(mState, mCount);
+ }
+}
+
+/*****************************************************************************
+* LuaFunction class
+*****************************************************************************/
+LuaFunction::LuaFunction(std::string_view name, lua_CFunction function,
+ std::string_view helptext)
+{
+ const auto& [registry, lookup] = getState();
+ registry.emplace(name, Registry::mapped_type{ function, helptext });
+ lookup.emplace(function, name);
+}
+
+void LuaFunction::init(lua_State* L)
+{
+ const auto& [registry, lookup] = getRState();
+ lluau_checkstack(L, 2);
+ // create LL table --
+ // it happens that we know exactly how many non-array members we want
+ lua_createtable(L, 0, int(narrow(lookup.size())));
+ int idx = lua_gettop(L);
+ for (const auto& [name, pair]: registry)
+ {
+ const auto& [funcptr, helptext] = pair;
+ // store funcptr in LL table with saved name
+ lua_pushcfunction(L, funcptr, name.c_str());
+ lua_setfield(L, idx, name.c_str());
+ }
+ // store LL in new lua_State's globals
+ lua_setglobal(L, "LL");
+}
+
+lua_CFunction LuaFunction::get(const std::string& key)
+{
+ // use find() instead of subscripting to avoid creating an entry for
+ // unknown key
+ const auto& [registry, lookup] = getState();
+ auto found{ registry.find(key) };
+ return (found == registry.end())? nullptr : found->second.first;
+}
+
+std::pair<LuaFunction::Registry&, LuaFunction::Lookup&> LuaFunction::getState()
+{
+ // use function-local statics to ensure they're initialized
+ static Registry registry;
+ static Lookup lookup;
+ return { registry, lookup };
+}
+
+/*****************************************************************************
+* source_path()
+*****************************************************************************/
+lua_function(source_path, "source_path(): return the source path of the running Lua script")
+{
+ lua_checkdelta(L, 1);
+ lluau_checkstack(L, 1);
+ lua_pushstdstring(L, lluau::source_path(L).u8string());
+ return 1;
+}
+
+/*****************************************************************************
+* source_dir()
+*****************************************************************************/
+lua_function(source_dir, "source_dir(): return the source directory of the running Lua script")
+{
+ lua_checkdelta(L, 1);
+ lluau_checkstack(L, 1);
+ lua_pushstdstring(L, lluau::source_path(L).parent_path().u8string());
+ return 1;
+}
+
+/*****************************************************************************
+* abspath()
+*****************************************************************************/
+lua_function(abspath, "abspath(path): "
+ "for given filesystem path relative to running script, return absolute path")
+{
+ lua_checkdelta(L);
+ auto path{ lua_tostdstring(L, 1) };
+ lua_pop(L, 1);
+ lua_pushstdstring(L, (lluau::source_path(L).parent_path() / path).u8string());
+ return 1;
+}
+
+/*****************************************************************************
+* check_stop()
+*****************************************************************************/
+lua_function(check_stop, "check_stop(): ensure that a Lua script responds to viewer shutdown")
+{
+ lua_checkdelta(L);
+ LLCoros::checkStop();
+ return 0;
+}
+
+/*****************************************************************************
+* help()
+*****************************************************************************/
+lua_function(help,
+ "LL.help(): list viewer's Lua functions\n"
+ "LL.help(function): show help string for specific function")
+{
+ auto& luapump{ LLEventPumps::instance().obtain("lua output") };
+ const auto& [registry, lookup]{ LuaFunction::getRState() };
+ if (! lua_gettop(L))
+ {
+ // no arguments passed: list all lua_functions
+ for (const auto& [name, pair] : registry)
+ {
+ const auto& [fptr, helptext] = pair;
+ luapump.post("LL." + helptext);
+ }
+ }
+ else
+ {
+ // arguments passed: list each of the specified lua_functions
+ for (int idx = 1, top = lua_gettop(L); idx <= top; ++idx)
+ {
+ std::string arg{ stringize("<unknown ", lua_typename(L, lua_type(L, idx)), ">") };
+ if (lua_type(L, idx) == LUA_TSTRING)
+ {
+ arg = lua_tostdstring(L, idx);
+ LLStringUtil::removePrefix(arg, "LL.");
+ }
+ else if (lua_type(L, idx) == LUA_TFUNCTION)
+ {
+ // Caller passed the actual function instead of its string
+ // name. A Lua function is an anonymous callable object; it
+ // has a name only by assigment. You can't ask Lua for a
+ // function's name, which is why our constructor maintains a
+ // reverse Lookup map.
+ auto function{ lua_tocfunction(L, idx) };
+ if (auto found = lookup.find(function); found != lookup.end())
+ {
+ // okay, pass found name to lookup below
+ arg = found->second;
+ }
+ }
+
+ if (auto found = registry.find(arg); found != registry.end())
+ {
+ luapump.post("LL." + found->second.second);
+ }
+ else
+ {
+ luapump.post(arg + ": NOT FOUND");
+ }
+ }
+ // pop all arguments
+ lua_settop(L, 0);
+ }
+ return 0; // void return
+}
+
+/*****************************************************************************
+* leaphelp()
+*****************************************************************************/
+lua_function(
+ leaphelp,
+ "LL.leaphelp(): list viewer's LEAP APIs\n"
+ "LL.leaphelp(api): show help for specific api string name")
+{
+ LLSD request;
+ int top{ lua_gettop(L) };
+ if (top)
+ {
+ request = llsd::map("op", "getAPI", "api", lua_tostdstring(L, 1));
+ }
+ else
+ {
+ request = llsd::map("op", "getAPIs");
+ }
+ // pop all args
+ lua_settop(L, 0);
+
+ auto& outpump{ LLEventPumps::instance().obtain("lua output") };
+ auto& listener{ LuaState::obtainListener(L) };
+ LLEventStream replyPump("leaphelp", true);
+ // ask the LuaListener's LeapListener and suspend calling coroutine until reply
+ auto reply{ llcoro::postAndSuspend(request, listener.getCommandName(), replyPump, "reply") };
+ reply.erase("reqid");
+
+ if (auto error = reply["error"]; error.isString())
+ {
+ outpump.post(error.asString());
+ return 0;
+ }
+
+ if (top)
+ {
+ // caller wants a specific API
+ outpump.post(stringize(reply["name"].asString(), ":\n", reply["desc"].asString()));
+ for (const auto& opmap : llsd::inArray(reply["ops"]))
+ {
+ std::ostringstream reqstr;
+ auto req{ opmap["required"] };
+ if (req.isArray())
+ {
+ const char* sep = " (requires ";
+ for (const auto& [reqkey, reqval] : llsd::inMap(req))
+ {
+ reqstr << sep << reqkey;
+ sep = ", ";
+ }
+ reqstr << ")";
+ }
+ outpump.post(stringize("---- ", reply["key"].asString(), " == '",
+ opmap["name"].asString(), "'", reqstr.str(), ":\n",
+ opmap["desc"].asString()));
+ }
+ }
+ else
+ {
+ // caller wants a list of APIs
+ for (const auto& [name, data] : llsd::inMap(reply))
+ {
+ outpump.post(stringize("==== ", name, ":\n", data["desc"].asString()));
+ }
+ }
+ return 0; // void return
+}
+
+/*****************************************************************************
+* setdtor
+*****************************************************************************/
+namespace {
+
+// proxy userdata object returned by setdtor()
+struct setdtor_refs
+{
+ lua_State* L;
+ std::string desc;
+ // You can't directly store a Lua object in a C++ object, but you can
+ // create a Lua "reference" by storing the object in the Lua Registry and
+ // capturing its Registry index.
+ int objref;
+ int dtorref;
+
+ setdtor_refs(lua_State* L, const std::string& desc, int objref, int dtorref):
+ L(L),
+ desc(desc),
+ objref(objref),
+ dtorref(dtorref)
+ {}
+ setdtor_refs(const setdtor_refs&) = delete;
+ setdtor_refs& operator=(const setdtor_refs&) = delete;
+ ~setdtor_refs();
+
+ static void push_metatable(lua_State* L);
+ static std::string binop(const std::string& name, const std::string& op);
+ static int meta__index(lua_State* L);
+};
+
+} // anonymous namespace
+
+lua_function(
+ setdtor,
+ "setdtor(desc, obj, dtorfunc) => proxy object referencing obj and dtorfunc.\n"
+ "When the returned proxy object is garbage-collected, or when the script\n"
+ "ends, call dtorfunc(obj). String desc is logged in the error message, if any.\n"
+ "Use the returned proxy object (or proxy._target) like obj.\n"
+ "obj won't be destroyed as long as the proxy exists; it's the proxy object's\n"
+ "lifespan that determines when dtorfunc(obj) will be called.")
+{
+ if (lua_gettop(L) != 3)
+ {
+ return lluau::error(L, "setdtor(desc, obj, dtor) requires exactly 3 arguments");
+ }
+ // called with (desc, obj, dtor), returns proxy object
+ lua_checkdelta(L, -2);
+// lluau_checkstack(L, 0); // might get up to 3 stack entries
+ auto desc{ lua_tostdstring(L, 1) };
+ // Get Lua "references" for each of the object and the dtor function.
+ int objref = lua_ref(L, 2);
+ int dtorref = lua_ref(L, 3);
+ // Having captured each of our parameters, discard them.
+ lua_settop(L, 0);
+ // Push our setdtor_refs userdata. Not only do we want to push it on L's
+ // stack, but setdtor_refs's constructor itself requires L.
+ lua_emplace<setdtor_refs>(L, L, desc, objref, dtorref);
+ // stack: proxy (i.e. setdtor_refs userdata)
+ // have to set its metatable
+ lua_getfield(L, LUA_REGISTRYINDEX, "setdtor_meta");
+ // stack: proxy, setdtor_meta (which might be nil)
+ if (lua_isnil(L, -1))
+ {
+ // discard nil
+ lua_pop(L, 1);
+ // compile and push our forwarding metatable
+ setdtor_refs::push_metatable(L);
+ // stack: proxy, metatable
+ // duplicate metatable to save it
+ lua_pushvalue(L, -1);
+ // stack: proxy, metatable, metable
+ // save metatable for future calls
+ lua_setfield(L, LUA_REGISTRYINDEX, "setdtor_meta");
+ // stack: proxy, metatable
+ }
+ // stack: proxy, metatable
+ lua_setmetatable(L, -2);
+ // stack: proxy
+ // Because ~setdtor_refs() necessarily uses the Lua stack, the Registry et
+ // al., we can't let a setdtor_refs instance be destroyed by lua_close():
+ // the Lua environment will already be partially shut down. To destroy
+ // this new setdtor_refs instance BEFORE lua_close(), bind it with
+ // lua_destroybounduserdata() and register it with LL.atexit().
+ // push (the entry point for) LL.atexit()
+ lua_pushcfunction(L, atexit_luasub::call, "LL.atexit()");
+ // stack: proxy, atexit()
+ lua_pushvalue(L, -2);
+ // stack: proxy, atexit(), proxy
+ int tag = lua_userdatatag(L, -1);
+ // We don't have a lookup table to get from an int Lua userdata tag to the
+ // corresponding C++ typeinfo name string. We'll introduce one if we need
+ // it for debugging. But for this particular call, we happen to know it's
+ // always a setdtor_refs object.
+ lua_pushcclosure(L, lua_destroybounduserdata,
+ stringize("lua_destroybounduserdata<", tag, ">()").c_str(),
+ 1);
+ // stack: proxy, atexit(), lua_destroybounduserdata
+ // call atexit(): one argument, no results, let error propagate
+ lua_call(L, 1, 0);
+ // stack: proxy
+ return 1;
+}
+
+namespace {
+
+void setdtor_refs::push_metatable(lua_State* L)
+{
+ lua_checkdelta(L, 1);
+ lluau_checkstack(L, 1);
+ // Ideally we want a metatable that forwards every operation on our
+ // setdtor_refs userdata proxy object to the original object. But the
+ // published C API doesn't include (e.g.) arithmetic operations on Lua
+ // objects, so in fact it's easier to express the desired metatable in Lua
+ // than in C++. We could make setdtor() depend on an external Lua module,
+ // but it seems less fragile to embed the Lua source code right here.
+ static const std::string setdtor_meta = stringize(R"-(
+ -- This metatable literal doesn't define __index() because that's
+ -- implemented in C++. We cannot, in Lua, peek into the setdtor_refs
+ -- userdata object to obtain objref, nor can we fetch Registry[objref].
+ -- So our C++ __index() metamethod recognizes access to '_target' as a
+ -- reference to Registry[objref].
+ -- The rest are defined per https://www.lua.org/manual/5.1/manual.html#2.8.
+ -- Luau supports destructors instead of __gc metamethod -- we rely on that!
+ -- We don't set __mode because our proxy is not a table. Real references
+ -- are stored in the wrapped table, so ITS __mode is what counts.
+ -- Initial definition of meta omits binary metamethods so they can bind the
+ -- metatable itself, as explained for binop() below.
+ local meta = {
+ __unm = function(arg)
+ return -arg._target
+ end,
+ __len = function(arg)
+ return #arg._target
+ end,
+ -- Comparison metamethods __eq(), __lt() and __le() are only called
+ -- when both operands have the same metamethod. For our purposes, that
+ -- means both operands are setdtor_refs userdata objects.
+ __eq = function(lhs, rhs)
+ return (lhs._target == rhs._target)
+ end,
+ __lt = function(lhs, rhs)
+ return (lhs._target < rhs._target)
+ end,
+ __le = function(lhs, rhs)
+ return (lhs._target <= rhs._target)
+ end,
+ __newindex = function(t, key, value)
+ assert(key ~= '_target',
+ "Don't try to replace a setdtor() proxy's _target")
+ t._target[key] = value
+ end,
+ __call = function(func, ...)
+ return func._target(...)
+ end,
+ __tostring = function(arg)
+ -- don't fret about arg._target's __tostring metamethod,
+ -- if any, because built-in tostring() deals with that
+ return tostring(arg._target)
+ end,
+ __iter = function(arg)
+ local iter = (getmetatable(arg._target) or {}).__iter
+ if iter then
+ return iter(arg._target)
+ else
+ return next, arg._target
+ end
+ end
+ }
+)-",
+ binop("add", "+"),
+ binop("sub", "-"),
+ binop("mul", "*"),
+ binop("div", "/"),
+ binop("idiv", "//"),
+ binop("mod", "%"),
+ binop("pow", "^"),
+ binop("concat", ".."),
+R"-(
+ return meta
+)-");
+ // only needed for debugging binop()
+// LL_DEBUGS("Lua") << setdtor_meta << LL_ENDL;
+
+ if (lluau::dostring(L, LL_PRETTY_FUNCTION, setdtor_meta) != LUA_OK)
+ {
+ // stack: error message string
+ lua_error(L);
+ }
+ llassert(lua_gettop(L) > 0);
+ llassert(lua_type(L, -1) == LUA_TTABLE);
+ // stack: Lua metatable compiled from setdtor_meta source
+ // Inject our C++ __index metamethod.
+ lua_rawsetfield(L, -1, "__index"sv, &setdtor_refs::meta__index);
+}
+
+// In the definition of setdtor_meta above, binary arithmethic and
+// concatenation metamethods are a little funny in that we don't know a
+// priori which operand is the userdata with our metatable: the metamethod
+// can be invoked either way. So every such metamethod must check, which
+// leads to lots of redundancy. Hence this helper function. Call it a Lua
+// macro.
+std::string setdtor_refs::binop(const std::string& name, const std::string& op)
+{
+ return stringize(
+ " meta.__", name, " = function(lhs, rhs)\n"
+ " if getmetatable(lhs) == meta then\n"
+ " return lhs._target ", op, " rhs\n"
+ " else\n"
+ " return lhs ", op, " rhs._target\n"
+ " end\n"
+ " end\n");
+}
+
+// setdtor_refs __index() metamethod
+int setdtor_refs::meta__index(lua_State* L)
+{
+ // called with (setdtor_refs userdata, key), returns retrieved object
+ lua_checkdelta(L, -1);
+ lluau_checkstack(L, 2);
+ // stack: proxy, key
+ // get ptr to the C++ struct data
+ auto ptr = lua_toclass<setdtor_refs>(L, -2);
+ // meta__index() should NEVER be called with anything but setdtor_refs!
+ llassert(ptr);
+ // push the wrapped object
+ lua_getref(L, ptr->objref);
+ // stack: proxy, key, _target
+ // replace userdata with _target
+ lua_replace(L, -3);
+ // stack: _target, key
+ // Duplicate key because lua_tostring() converts number to string:
+ // if the key is (e.g.) 1, don't try to retrieve _target["1"]!
+ lua_pushvalue(L, -1);
+ // stack: _target, key, key
+ // recognize the special _target field
+ if (lua_tostdstring(L, -1) == "_target")
+ {
+ // okay, ditch both copies of "_target" string key
+ lua_pop(L, 2);
+ // stack: _target
+ }
+ else // any key but _target
+ {
+ // ditch stringized key
+ lua_pop(L, 1);
+ // stack: _target, key
+ // replace key with _target[key], invoking metamethod if any
+ lua_gettable(L, -2);
+ // stack: _target, _target[key]
+ // discard _target
+ lua_remove(L, -2);
+ // stack: _target[key]
+ }
+ return 1;
+}
+
+// replacement for global next():
+// its lua_upvalueindex(1) is the original function it's replacing
+int lua_proxydrill(lua_State* L)
+{
+ // Accept however many arguments the original function normally accepts.
+ // If our first arg is a userdata, check if it's a setdtor_refs proxy.
+ // Drill through as many levels of proxy wrapper as needed.
+ while (const setdtor_refs* ptr = lua_toclass<setdtor_refs>(L, 1))
+ {
+ // push original object
+ lua_getref(L, ptr->objref);
+ // replace first argument with that
+ lua_replace(L, 1);
+ }
+ // We've reached a first argument that's not a setdtor() proxy.
+ // How many arguments were we passed, anyway?
+ int args = lua_gettop(L);
+ // Push the original function, captured as our upvalue.
+ lua_pushvalue(L, lua_upvalueindex(1));
+ // Shift the stack so the original function is first.
+ lua_insert(L, 1);
+ // Call the original function with all original args, no error checking.
+ // Don't truncate however many values that function returns.
+ lua_call(L, args, LUA_MULTRET);
+ // Return as many values as the original function returned.
+ return lua_gettop(L);
+}
+
+// When Lua destroys a setdtor_refs userdata object, either from garbage
+// collection or from LL.atexit(lua_destroybounduserdata), it's time to keep
+// its promise to call the specified Lua destructor function with the
+// specified Lua object. Of course we must also delete the captured
+// "references" to both objects.
+setdtor_refs::~setdtor_refs()
+{
+ lua_checkdelta(L);
+ lluau_checkstack(L, 2);
+ // push Registry[dtorref]
+ lua_getref(L, dtorref);
+ // push Registry[objref]
+ lua_getref(L, objref);
+ // free Registry[dtorref]
+ lua_unref(L, dtorref);
+ // free Registry[objref]
+ lua_unref(L, objref);
+ // call dtor(obj): one arg, no result, no error function
+ int rc = lua_pcall(L, 1, 0, 0);
+ if (rc != LUA_OK)
+ {
+ // TODO: we don't really want to propagate the error here.
+ // If this setdtor_refs instance is being destroyed by
+ // LL.atexit(), we want to continue cleanup. If it's being
+ // garbage-collected, the call is completely unpredictable from
+ // the consuming script's point of view. But what to do about this
+ // error?? For now, just log it.
+ LL_WARNS("Lua") << LLCoros::getName()
+ << ": setdtor(" << std::quoted(desc) << ") error: "
+ << lua_tostring(L, -1) << LL_ENDL;
+ lua_pop(L, 1);
+ }
+}
+
+} // anonymous namespace
+
+/*****************************************************************************
+* lua_what
+*****************************************************************************/
+std::ostream& operator<<(std::ostream& out, const lua_what& self)
+{
+ switch (lua_type(self.L, self.index))
+ {
+ case LUA_TNONE:
+ // distinguish acceptable but non-valid index
+ out << "none";
+ break;
+
+ case LUA_TNIL:
+ out << "nil";
+ break;
+
+ case LUA_TBOOLEAN:
+ {
+ auto oldflags { out.flags() };
+ out << std::boolalpha << lua_toboolean(self.L, self.index);
+ out.flags(oldflags);
+ break;
+ }
+
+ case LUA_TNUMBER:
+ out << lua_tonumber(self.L, self.index);
+ break;
+
+ case LUA_TSTRING:
+ out << std::quoted(lua_tostdstring(self.L, self.index));
+ break;
+
+ case LUA_TUSERDATA:
+ {
+ const S32 maxlen = 20;
+ S32 binlen{ lua_rawlen(self.L, self.index) };
+ LLSD::Binary binary(std::min(maxlen, binlen));
+ std::memcpy(binary.data(), lua_touserdata(self.L, self.index), binary.size());
+ out << LL::hexdump(binary);
+ if (binlen > maxlen)
+ {
+ out << "...(" << (binlen - maxlen) << " more)";
+ }
+ break;
+ }
+
+ case LUA_TLIGHTUSERDATA:
+ out << lua_touserdata(self.L, self.index);
+ break;
+
+ case LUA_TFUNCTION:
+ {
+ // Try for the function's name, at the cost of a few more stack
+ // entries.
+ lua_checkdelta(self.L);
+ lluau_checkstack(self.L, 3);
+ lua_getglobal(self.L, "debug");
+ // stack: ..., debug
+ lua_getfield(self.L, -1, "info");
+ // stack: ..., debug, debug.info
+ lua_remove(self.L, -2);
+ // stack: ..., debug.info
+ lua_pushvalue(self.L, self.index);
+ // stack: ..., debug.info, this function
+ lua_pushstring(self.L, "n");
+ // stack: ..., debug.info, this function, "n"
+ // 2 arguments, 1 return value (or error message), no error handler
+ lua_pcall(self.L, 2, 1, 0);
+ // stack: ..., function name (or error) from debug.info()
+ out << "function " << lua_tostdstring(self.L, -1);
+ lua_pop(self.L, 1);
+ // stack: ...
+ break;
+ }
+
+ default:
+ // anything else, don't bother trying to report value, just type
+ out << lua_typename(self.L, lua_type(self.L, self.index));
+ break;
+ }
+ return out;
+}
+
+/*****************************************************************************
+* lua_stack
+*****************************************************************************/
+std::ostream& operator<<(std::ostream& out, const lua_stack& self)
+{
+ out << "stack: [";
+ const char* sep = "";
+ for (int index = 1; index <= lua_gettop(self.L); ++index)
+ {
+ out << sep << lua_what(self.L, index);
+ sep = ", ";
+ }
+ out << ']';
+ return out;
+}
+
+/*****************************************************************************
+* LuaStackDelta
+*****************************************************************************/
+LuaStackDelta::LuaStackDelta(lua_State* L, const std::string& where, int delta):
+ L(L),
+ mWhere(where),
+ mDepth(lua_gettop(L)),
+ mDelta(delta)
+{}
+
+LuaStackDelta::~LuaStackDelta()
+{
+ auto depth{ lua_gettop(L) };
+ // If we're unwinding the stack due to an exception, then of course we
+ // can't expect the logic in the block containing this LuaStackDelta
+ // instance to keep its contract wrt the Lua data stack.
+ if (std::uncaught_exceptions() == 0 && mDepth + mDelta != depth)
+ {
+ LL_ERRS("Lua") << LLCoros::getName() << ": " << mWhere
+ << ": Lua stack went from " << mDepth << " to " << depth;
+ if (mDelta)
+ {
+ LL_CONT << ", rather than expected " << (mDepth + mDelta) << " (" << mDelta << ")";
+ }
+ LL_ENDL;
+ }
+}
diff --git a/indra/llcommon/lua_function.h b/indra/llcommon/lua_function.h
new file mode 100644
index 0000000000..10c201c234
--- /dev/null
+++ b/indra/llcommon/lua_function.h
@@ -0,0 +1,619 @@
+/**
+ * @file lua_function.h
+ * @author Nat Goodspeed
+ * @date 2024-02-05
+ * @brief Definitions useful for coding a new Luau entry point into C++
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Copyright (c) 2024, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_LUA_FUNCTION_H)
+#define LL_LUA_FUNCTION_H
+
+#include "luau/luacode.h"
+#include "luau/lua.h"
+#include "luau/luaconf.h"
+#include "luau/lualib.h"
+#include "fsyspath.h"
+#include "llerror.h"
+#include "llsd.h"
+#include "stringize.h"
+#include <exception> // std::uncaught_exceptions()
+#include <memory> // std::shared_ptr
+#include <typeindex>
+#include <typeinfo>
+#include <unordered_map>
+#include <utility> // std::pair
+
+class LuaListener;
+
+/*****************************************************************************
+* lluau namespace utility functions
+*****************************************************************************/
+namespace lluau
+{
+ // luau defines luaL_error() as void, but we want to use the Lua idiom of
+ // 'return error(...)'. Wrap luaL_error() in an int function.
+#if __clang__
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wformat-security"
+#endif // __clang__
+ template<typename... Args>
+ int error(lua_State* L, const char* format, Args&&... args)
+ {
+ luaL_error(L, format, std::forward<Args>(args)...);
+#ifndef LL_MSVC
+ return 0;
+#endif
+ }
+#if __clang__
+#pragma clang diagnostic pop
+#endif // __clang__
+
+ // luau removed lua_dostring(), but since we perform the equivalent luau
+ // sequence in multiple places, encapsulate it. desc and text are strings
+ // rather than string_views because dostring() needs pointers to nul-
+ // terminated char arrays.
+ int dostring(lua_State* L, const std::string& desc, const std::string& text);
+ int loadstring(lua_State* L, const std::string& desc, const std::string& text);
+
+ fsyspath source_path(lua_State* L);
+} // namespace lluau
+
+// must be a macro because LL_PRETTY_FUNCTION is context-sensitive
+#define lluau_checkstack(L, n) luaL_checkstack((L), (n), LL_PRETTY_FUNCTION)
+
+std::string lua_tostdstring(lua_State* L, int index);
+void lua_pushstdstring(lua_State* L, const std::string& str);
+LLSD lua_tollsd(lua_State* L, int index);
+void lua_pushllsd(lua_State* L, const LLSD& data);
+
+/*****************************************************************************
+* LuaState
+*****************************************************************************/
+/**
+ * RAII class to manage the lifespan of a lua_State
+ */
+class LuaState
+{
+public:
+ typedef std::function<void(std::string msg)> script_finished_fn;
+
+ LuaState(script_finished_fn cb={});
+
+ LuaState(const LuaState&) = delete;
+ LuaState& operator=(const LuaState&) = delete;
+
+ ~LuaState();
+
+ bool checkLua(const std::string& desc, int r);
+
+ // expr() is for when we want to capture any results left on the stack
+ // by a Lua expression, possibly including multiple return values.
+ // int < 0 means error, and LLSD::asString() is the error message.
+ // int == 0 with LLSD::isUndefined() means the Lua expression returned no
+ // results.
+ // int == 1 means the Lua expression returned one result.
+ // int > 1 with LLSD::isArray() means the Lua expression returned
+ // multiple results, represented as the entries of the array.
+ std::pair<int, LLSD> expr(const std::string& desc, const std::string& text);
+
+ operator lua_State*() const { return mState; }
+
+ // Find or create LuaListener for this LuaState.
+ LuaListener& obtainListener() { return obtainListener(mState); }
+ // Find or create LuaListener for passed lua_State.
+ static LuaListener& obtainListener(lua_State* L);
+
+ // Given lua_State* L, return the LuaState object managing (the main Lua
+ // thread for) L.
+ static LuaState& getParent(lua_State* L);
+
+ void set_interrupts_counter(S32 counter);
+ void check_interrupts_counter();
+
+private:
+ /*---------------------------- feature flag ----------------------------*/
+ bool mFeature{ false };
+ /*---------------------------- feature flag ----------------------------*/
+ script_finished_fn mCallback;
+ lua_State* mState{ nullptr };
+ std::string mError;
+ S32 mInterrupts{ 0 };
+};
+
+/*****************************************************************************
+* LuaPopper
+*****************************************************************************/
+/**
+ * LuaPopper is an RAII class whose role is to pop some number of entries
+ * from the Lua stack if the calling function exits early.
+ */
+class LuaPopper
+{
+public:
+ LuaPopper(lua_State* L, int count):
+ mState(L),
+ mCount(count)
+ {}
+
+ LuaPopper(const LuaPopper&) = delete;
+ LuaPopper& operator=(const LuaPopper&) = delete;
+
+ ~LuaPopper();
+
+ void disarm() { set(0); }
+ void set(int count) { mCount = count; }
+
+private:
+ lua_State* mState;
+ int mCount;
+};
+
+/*****************************************************************************
+* LuaRemover
+*****************************************************************************/
+/**
+ * Remove a particular stack index on exit from enclosing scope.
+ * If you pass a negative index (meaning relative to the current stack top),
+ * converts to an absolute index. The point of LuaRemover is to remove the
+ * entry at the specified index regardless of subsequent pushes to the stack.
+ */
+class LuaRemover
+{
+public:
+ LuaRemover(lua_State* L, int index):
+ mState(L),
+ mIndex(lua_absindex(L, index))
+ {}
+ LuaRemover(const LuaRemover&) = delete;
+ LuaRemover& operator=(const LuaRemover&) = delete;
+ ~LuaRemover()
+ {
+ // If we're unwinding the C++ stack due to an exception, don't mess
+ // with the Lua stack!
+ if (std::uncaught_exceptions() == 0)
+ lua_remove(mState, mIndex);
+ }
+
+private:
+ lua_State* mState;
+ int mIndex;
+};
+
+/*****************************************************************************
+* LuaStackDelta
+*****************************************************************************/
+/**
+ * Instantiate LuaStackDelta in a block to compare the Lua data stack depth on
+ * entry (LuaStackDelta construction) and exit. Optionally, pass the expected
+ * depth increment. (But be aware that LuaStackDelta cannot observe the effect
+ * of a LuaPopper or LuaRemover declared previously in the same block.)
+ */
+class LuaStackDelta
+{
+public:
+ LuaStackDelta(lua_State* L, const std::string& where, int delta=0);
+ LuaStackDelta(const LuaStackDelta&) = delete;
+ LuaStackDelta& operator=(const LuaStackDelta&) = delete;
+
+ ~LuaStackDelta();
+
+private:
+ lua_State* L;
+ std::string mWhere;
+ int mDepth, mDelta;
+};
+
+#define lua_checkdelta(L, ...) LuaStackDelta delta(L, LL_PRETTY_FUNCTION, ##__VA_ARGS__)
+
+/*****************************************************************************
+* lua_push() wrappers for generic code
+*****************************************************************************/
+inline
+void lua_push(lua_State* L, bool b)
+{
+ lua_pushboolean(L, int(b));
+}
+
+inline
+void lua_push(lua_State* L, lua_CFunction fn)
+{
+ lua_pushcfunction(L, fn, "");
+}
+
+inline
+void lua_push(lua_State* L, lua_Integer n)
+{
+ lua_pushinteger(L, n);
+}
+
+inline
+void lua_push(lua_State* L, void* p)
+{
+ lua_pushlightuserdata(L, p);
+}
+
+inline
+void lua_push(lua_State* L, const LLSD& data)
+{
+ lua_pushllsd(L, data);
+}
+
+inline
+void lua_push(lua_State* L, const char* s, size_t len)
+{
+ lua_pushlstring(L, s, len);
+}
+
+inline
+void lua_push(lua_State* L)
+{
+ lua_pushnil(L);
+}
+
+inline
+void lua_push(lua_State* L, lua_Number n)
+{
+ lua_pushnumber(L, n);
+}
+
+inline
+void lua_push(lua_State* L, const std::string& s)
+{
+ lua_pushstdstring(L, s);
+}
+
+inline
+void lua_push(lua_State* L, const char* s)
+{
+ lua_pushstring(L, s);
+}
+
+/*****************************************************************************
+* lua_to() wrappers for generic code
+*****************************************************************************/
+template <typename T>
+auto lua_to(lua_State* L, int index);
+
+template <>
+inline
+auto lua_to<bool>(lua_State* L, int index)
+{
+ return lua_toboolean(L, index);
+}
+
+template <>
+inline
+auto lua_to<lua_CFunction>(lua_State* L, int index)
+{
+ return lua_tocfunction(L, index);
+}
+
+template <>
+inline
+auto lua_to<lua_Integer>(lua_State* L, int index)
+{
+ return lua_tointeger(L, index);
+}
+
+template <>
+inline
+auto lua_to<LLSD>(lua_State* L, int index)
+{
+ return lua_tollsd(L, index);
+}
+
+template <>
+inline
+auto lua_to<lua_Number>(lua_State* L, int index)
+{
+ return lua_tonumber(L, index);
+}
+
+template <>
+inline
+auto lua_to<std::string>(lua_State* L, int index)
+{
+ return lua_tostdstring(L, index);
+}
+
+template <>
+inline
+auto lua_to<void*>(lua_State* L, int index)
+{
+ return lua_touserdata(L, index);
+}
+
+/*****************************************************************************
+* field operations
+*****************************************************************************/
+// return to C++, from table at index, the value of field k
+template <typename T>
+auto lua_getfieldv(lua_State* L, int index, const char* k)
+{
+ lua_checkdelta(L);
+ lluau_checkstack(L, 1);
+ lua_getfield(L, index, k);
+ LuaPopper pop(L, 1);
+ return lua_to<T>(L, -1);
+}
+
+// set in table at index, as field k, the specified C++ value
+template <typename T>
+auto lua_setfieldv(lua_State* L, int index, const char* k, const T& value)
+{
+ index = lua_absindex(L, index);
+ lua_checkdelta(L);
+ lluau_checkstack(L, 1);
+ lua_push(L, value);
+ lua_setfield(L, index, k);
+}
+
+// return to C++, from table at index, the value of field k (without metamethods)
+template <typename T>
+auto lua_rawgetfield(lua_State* L, int index, std::string_view k)
+{
+ index = lua_absindex(L, index);
+ lua_checkdelta(L);
+ lluau_checkstack(L, 1);
+ lua_pushlstring(L, k.data(), k.length());
+ lua_rawget(L, index);
+ LuaPopper pop(L, 1);
+ return lua_to<T>(L, -1);
+}
+
+// set in table at index, as field k, the specified C++ value (without metamethods)
+template <typename T>
+void lua_rawsetfield(lua_State* L, int index, std::string_view k, const T& value)
+{
+ index = lua_absindex(L, index);
+ lua_checkdelta(L);
+ lluau_checkstack(L, 2);
+ lua_pushlstring(L, k.data(), k.length());
+ lua_push(L, value);
+ lua_rawset(L, index);
+}
+
+/*****************************************************************************
+* lua_function (and helper class LuaFunction)
+*****************************************************************************/
+/**
+ * LuaFunction is a base class containing a static registry of its static
+ * subclass call() methods. call() is NOT virtual: instead, each subclass
+ * constructor passes a pointer to its distinct call() method to the base-
+ * class constructor, along with a name by which to register that method.
+ *
+ * The init() method walks the registry and registers each such name with the
+ * passed lua_State.
+ */
+class LuaFunction
+{
+public:
+ LuaFunction(std::string_view name, lua_CFunction function,
+ std::string_view helptext);
+
+ static void init(lua_State* L);
+
+ static lua_CFunction get(const std::string& key);
+
+protected:
+ using Registry = std::map<std::string, std::pair<lua_CFunction, std::string>>;
+ using Lookup = std::map<lua_CFunction, std::string>;
+ static std::pair<const Registry&, const Lookup&> getRState() { return getState(); }
+
+private:
+ static std::pair<Registry&, Lookup&> getState();
+};
+
+/**
+ * lua_function(name, helptext) is a macro to facilitate defining C++ functions
+ * available to Lua. It defines a subclass of LuaFunction and declares a
+ * static instance of that subclass, thereby forcing the compiler to call its
+ * constructor at module initialization time. The constructor passes the
+ * stringized instance name to its LuaFunction base-class constructor, along
+ * with a pointer to the static subclass call() method. It then emits the
+ * call() method definition header, to be followed by a method body enclosed
+ * in curly braces as usual.
+ */
+#define lua_function(name, helptext) \
+static struct name##_luasub : public LuaFunction \
+{ \
+ name##_luasub(): LuaFunction(#name, &call, helptext) {} \
+ static int call(lua_State* L); \
+} name##_lua; \
+int name##_luasub::call(lua_State* L)
+// {
+// ... supply method body here, referencing 'L' ...
+// }
+
+/*****************************************************************************
+* lua_emplace<T>(), lua_toclass<T>()
+*****************************************************************************/
+// Every instance of DistinctInt has a different int value, barring int
+// wraparound.
+class DistinctInt
+{
+public:
+ DistinctInt(): mValue(++mValues) {}
+ int get() const { return mValue; }
+ operator int() const { return mValue; }
+private:
+ static int mValues;
+ int mValue;
+};
+
+namespace {
+
+template <typename T>
+struct TypeTag
+{
+ // For (std::is_same<T, U>), &TypeTag<T>::value == &TypeTag<U>::value.
+ // For (! std::is_same<T, U>), &TypeTag<T>::value != &TypeTag<U>::value.
+ // And every distinct instance of DistinctInt has a distinct value.
+ // Therefore, TypeTag<T>::value is an int uniquely associated with each
+ // distinct T.
+ static DistinctInt value;
+};
+
+template <typename T>
+DistinctInt TypeTag<T>::value;
+
+} // anonymous namespace
+
+/**
+ * On the stack belonging to the passed lua_State, push a Lua userdata object
+ * containing a newly-constructed C++ object T(args...). The userdata has a
+ * Luau destructor guaranteeing that the new T instance is destroyed when the
+ * userdata is garbage-collected, no later than when the LuaState is
+ * destroyed. It may be destroyed explicitly by calling lua_destroyuserdata().
+ *
+ * Usage:
+ * lua_emplace<T>(L, T constructor args...);
+ * // L's Lua stack top is now a userdata containing T
+ */
+template <class T, typename... ARGS>
+void lua_emplace(lua_State* L, ARGS&&... args)
+{
+ lua_checkdelta(L, 1);
+ lluau_checkstack(L, 1);
+ int tag{ TypeTag<T>::value };
+ if (! lua_getuserdatadtor(L, tag))
+ {
+ // We haven't yet told THIS lua_State the destructor to use for this tag.
+ lua_setuserdatadtor(
+ L, tag,
+ [](lua_State*, void* ptr)
+ {
+ // destroy the contained T instance
+ static_cast<T*>(ptr)->~T();
+ });
+ }
+ auto ptr = lua_newuserdatatagged(L, sizeof(T), tag);
+ // stack is uninitialized userdata
+ // For now, assume (but verify) that lua_newuserdata() returns a
+ // conservatively-aligned ptr. If that turns out not to be the case, we
+ // might have to discard the new userdata, overallocate its successor and
+ // perform manual alignment -- but only if we must.
+ llassert((uintptr_t(ptr) % alignof(T)) == 0);
+ // Construct our T there using placement new
+ new (ptr) T(std::forward<ARGS>(args)...);
+ // stack is now initialized userdata containing our T instance -- return
+ // that
+}
+
+/**
+ * If the value at the passed acceptable index is a full userdata created by
+ * lua_emplace<T>(), return a pointer to the contained T instance. Otherwise
+ * (index is not a full userdata; userdata is not of type T) return nullptr.
+ */
+template <class T>
+T* lua_toclass(lua_State* L, int index)
+{
+ lua_checkdelta(L);
+ // get void* pointer to userdata (if that's what it is)
+ void* ptr{ lua_touserdatatagged(L, index, TypeTag<T>::value) };
+ // Derive the T* from ptr. If in future lua_emplace() must manually
+ // align our T* within the Lua-provided void*, adjust accordingly.
+ return static_cast<T*>(ptr);
+}
+
+/**
+ * Call lua_destroyuserdata() with the doomed userdata on the stack top.
+ * It must have been created by lua_emplace().
+ */
+int lua_destroyuserdata(lua_State* L);
+
+/**
+ * Call lua_pushcclosure(L, lua_destroybounduserdata, 1) with the target
+ * userdata on the stack top. When the resulting C closure is called with no
+ * arguments, the bound userdata is destroyed by lua_destroyuserdata().
+ */
+int lua_destroybounduserdata(lua_State *L);
+
+/*****************************************************************************
+* lua_what()
+*****************************************************************************/
+// Usage: std::cout << lua_what(L, stackindex) << ...;
+// Reports on the Lua value found at the passed stackindex.
+// If cast to std::string, returns the corresponding string value.
+class lua_what
+{
+public:
+ lua_what(lua_State* state, int idx):
+ L(state),
+ index(idx)
+ {}
+
+ friend std::ostream& operator<<(std::ostream& out, const lua_what& self);
+
+ operator std::string() const { return stringize(*this); }
+
+private:
+ lua_State* L;
+ int index;
+};
+
+/*****************************************************************************
+* lua_stack()
+*****************************************************************************/
+// Usage: std::cout << lua_stack(L) << ...;
+// Reports on the contents of the Lua stack.
+// If cast to std::string, returns the corresponding string value.
+class lua_stack
+{
+public:
+ lua_stack(lua_State* state):
+ L(state)
+ {}
+
+ friend std::ostream& operator<<(std::ostream& out, const lua_stack& self);
+
+ operator std::string() const { return stringize(*this); }
+
+private:
+ lua_State* L;
+};
+
+/*****************************************************************************
+* LuaLog
+*****************************************************************************/
+// adapted from indra/test/debug.h
+// can't generalize Debug::operator() target because it's a variadic template
+class LuaLog
+{
+public:
+ template <typename... ARGS>
+ LuaLog(lua_State* L, ARGS&&... args):
+ L(L),
+ mBlock(stringize(std::forward<ARGS>(args)...))
+ {
+ (*this)("entry ", lua_stack(L));
+ }
+
+ // non-copyable
+ LuaLog(const LuaLog&) = delete;
+ LuaLog& operator=(const LuaLog&) = delete;
+
+ ~LuaLog()
+ {
+ auto exceptional{ std::uncaught_exceptions()? "exceptional " : "" };
+ (*this)(exceptional, "exit ", lua_stack(L));
+ }
+
+ template <typename... ARGS>
+ void operator()(ARGS&&... args)
+ {
+ LL_DEBUGS("Lua") << mBlock << ' ';
+ stream_to(LL_CONT, std::forward<ARGS>(args)...);
+ LL_ENDL;
+ }
+
+private:
+ lua_State* L;
+ const std::string mBlock;
+};
+
+#endif /* ! defined(LL_LUA_FUNCTION_H) */
diff --git a/indra/llcommon/lualistener.cpp b/indra/llcommon/lualistener.cpp
new file mode 100644
index 0000000000..94085c6798
--- /dev/null
+++ b/indra/llcommon/lualistener.cpp
@@ -0,0 +1,102 @@
+/**
+ * @file lualistener.cpp
+ * @author Nat Goodspeed
+ * @date 2024-02-06
+ * @brief Implementation for lualistener.
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Copyright (c) 2024, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "lualistener.h"
+// STL headers
+// std headers
+#include <iomanip> // std::quoted()
+// external library headers
+#include "luau/lua.h"
+// other Linden headers
+#include "llerror.h"
+#include "llleaplistener.h"
+#include "lua_function.h"
+
+const int MAX_QSIZE = 1000;
+
+std::ostream& operator<<(std::ostream& out, const LuaListener& self)
+{
+ return out << "LuaListener(" << std::quoted(self.mCoroName) << ", "
+ << self.getReplyName() << ", " << self.getCommandName() << ")";
+}
+
+LuaListener::LuaListener(lua_State* L):
+ mCoroName(LLCoros::getName()),
+ mListener(new LLLeapListener(
+ "LuaListener",
+ [this](const std::string& pump, const LLSD& data)
+ { return queueEvent(pump, data); })),
+ // Listen for shutdown events.
+ mShutdownConnection(
+ LLCoros::getStopListener(
+ "LuaState",
+ mCoroName,
+ [this](const LLSD&)
+ {
+ // If a Lua script is still blocked in getNext() during
+ // viewer shutdown, close the queue to wake up getNext().
+ mQueue.close();
+ }))
+{
+ LL_DEBUGS("Lua") << "LuaListener(" << std::quoted(mCoroName) << ")" << LL_ENDL;
+}
+
+LuaListener::~LuaListener()
+{
+ LL_DEBUGS("Lua") << "~LuaListener(" << std::quoted(mCoroName) << ")" << LL_ENDL;
+}
+
+std::string LuaListener::getReplyName() const
+{
+ return mListener->getReplyPump().getName();
+}
+
+std::string LuaListener::getCommandName() const
+{
+ return mListener->getPumpName();
+}
+
+bool LuaListener::queueEvent(const std::string& pump, const LLSD& data)
+{
+ // Our Lua script might be stalled, or just fail to retrieve events. Don't
+ // grow this queue indefinitely. But don't set MAX_QSIZE as the queue
+ // capacity or we'd block the post() call trying to propagate this event!
+ if (auto size = mQueue.size(); size > MAX_QSIZE)
+ {
+ LL_WARNS("Lua") << "LuaListener queue for " << mCoroName
+ << " exceeds " << MAX_QSIZE << ": " << size
+ << " -- discarding event" << LL_ENDL;
+ }
+ else
+ {
+ mQueue.push(decltype(mQueue)::value_type(pump, data));
+ }
+ return false;
+}
+
+LuaListener::PumpData LuaListener::getNext()
+{
+ try
+ {
+ LLCoros::TempStatus status("get_event_next()");
+ return mQueue.pop();
+ }
+ catch (const LLThreadSafeQueueInterrupt&)
+ {
+ // mQueue has been closed. The only way that happens is when we detect
+ // viewer shutdown. Terminate the calling Lua coroutine.
+ LLCoros::checkStop();
+ return {};
+ }
+}
diff --git a/indra/llcommon/lualistener.h b/indra/llcommon/lualistener.h
new file mode 100644
index 0000000000..4c0a2a5c87
--- /dev/null
+++ b/indra/llcommon/lualistener.h
@@ -0,0 +1,65 @@
+/**
+ * @file lualistener.h
+ * @author Nat Goodspeed
+ * @date 2024-02-06
+ * @brief Define LuaListener class
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Copyright (c) 2024, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_LUALISTENER_H)
+#define LL_LUALISTENER_H
+
+#include "llevents.h" // LLTempBoundListener
+#include "llsd.h"
+#include "llthreadsafequeue.h"
+#include <iosfwd> // std::ostream
+#include <memory> // std::unique_ptr
+#include <string>
+#include <utility> // std::pair
+
+struct lua_State;
+class LLLeapListener;
+
+/**
+ * LuaListener is based on LLLeap. It serves an analogous function.
+ *
+ * Like LLLeap, each LuaListener instance also has an associated
+ * LLLeapListener to respond to LLEventPump management commands.
+ */
+class LuaListener
+{
+public:
+ LuaListener(lua_State* L);
+
+ LuaListener(const LuaListener&) = delete;
+ LuaListener& operator=(const LuaListener&) = delete;
+
+ ~LuaListener();
+
+ std::string getReplyName() const;
+ std::string getCommandName() const;
+
+ /**
+ * LuaListener enqueues reply events from its LLLeapListener on mQueue.
+ * Call getNext() to retrieve the next such event. Blocks the calling
+ * coroutine if the queue is empty.
+ */
+ using PumpData = std::pair<std::string, LLSD>;
+ PumpData getNext();
+
+ friend std::ostream& operator<<(std::ostream& out, const LuaListener& self);
+
+private:
+ bool queueEvent(const std::string& pump, const LLSD& data);
+
+ LLThreadSafeQueue<PumpData> mQueue;
+
+ std::string mCoroName;
+ std::unique_ptr<LLLeapListener> mListener;
+ LLTempBoundListener mShutdownConnection;
+};
+
+#endif /* ! defined(LL_LUALISTENER_H) */
diff --git a/indra/llcommon/resultset.cpp b/indra/llcommon/resultset.cpp
new file mode 100644
index 0000000000..4d7b00eabd
--- /dev/null
+++ b/indra/llcommon/resultset.cpp
@@ -0,0 +1,96 @@
+/**
+ * @file resultset.cpp
+ * @author Nat Goodspeed
+ * @date 2024-09-03
+ * @brief Implementation for resultset.
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Copyright (c) 2024, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "resultset.h"
+// STL headers
+// std headers
+// external library headers
+// other Linden headers
+#include "llerror.h"
+#include "llsdutil.h"
+
+namespace LL
+{
+
+LLSD ResultSet::getKeyLength() const
+{
+ return llsd::array(getKey(), getLength());
+}
+
+std::pair<LLSD, int> ResultSet::getSliceStart(int index, int count) const
+{
+ // only call getLength() once
+ auto length = getLength();
+ // Adjust bounds [start, end) to overlap the actual result set from
+ // [0, getLength()). Permit negative index; e.g. with a result set
+ // containing 5 entries, getSlice(-2, 5) will adjust start to 0 and
+ // end to 3.
+ int start = llclamp(index, 0, length);
+ int end = llclamp(index + count, 0, length);
+ LLSD result{ LLSD::emptyArray() };
+ // beware of count <= 0, or an [index, count) range that doesn't even
+ // overlap [0, length) at all
+ if (end > start)
+ {
+ // Right away expand the result array to the size we'll need.
+ // (end - start) is that size; (end - start - 1) is the index of the
+ // last entry in result.
+ result[end - start - 1] = LLSD();
+ for (int i = 0; (start + i) < end; ++i)
+ {
+ // For this to be a slice, set result[0] = getSingle(start), etc.
+ result[i] = getSingle(start + i);
+ }
+ }
+ return { result, start };
+}
+
+LLSD ResultSet::getSlice(int index, int count) const
+{
+ return getSliceStart(index, count).first;
+}
+
+/*==========================================================================*|
+LLSD ResultSet::getSingle(int index) const
+{
+ if (0 <= index && index < getLength())
+ {
+ return getSingle_(index);
+ }
+ else
+ {
+ return {};
+ }
+}
+|*==========================================================================*/
+
+ResultSet::ResultSet(const std::string& name):
+ mName(name)
+{
+ LL_DEBUGS("Lua") << *this << LL_ENDL;
+}
+
+ResultSet::~ResultSet()
+{
+ // We want to be able to observe that the consuming script eventually
+ // destroys each of these ResultSets.
+ LL_DEBUGS("Lua") << "~" << *this << LL_ENDL;
+}
+
+} // namespace LL
+
+std::ostream& operator<<(std::ostream& out, const LL::ResultSet& self)
+{
+ return out << "ResultSet(" << self.mName << ", " << self.getKey() << ")";
+}
diff --git a/indra/llcommon/resultset.h b/indra/llcommon/resultset.h
new file mode 100644
index 0000000000..90d52b6fe4
--- /dev/null
+++ b/indra/llcommon/resultset.h
@@ -0,0 +1,61 @@
+/**
+ * @file resultset.h
+ * @author Nat Goodspeed
+ * @date 2024-09-03
+ * @brief ResultSet is an abstract base class to allow scripted access to
+ * potentially large collections representable as LLSD arrays.
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Copyright (c) 2024, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_RESULTSET_H)
+#define LL_RESULTSET_H
+
+#include "llinttracker.h"
+#include "llsd.h"
+#include <iosfwd> // std::ostream
+#include <utility> // std::pair
+
+namespace LL
+{
+
+// This abstract base class defines an interface by which a large collection
+// of items representable as an LLSD array can be retrieved in slices. It isa
+// LLIntTracker so we can pass its unique int key to a consuming script via
+// LLSD.
+struct ResultSet: public LLIntTracker<ResultSet>
+{
+ // Get the length of the result set. Indexes are 0-relative.
+ virtual int getLength() const = 0;
+ // Get conventional LLSD { key, length } pair.
+ LLSD getKeyLength() const;
+ // Retrieve LLSD corresponding to a single entry from the result set,
+ // once we're sure the index is valid.
+ virtual LLSD getSingle(int index) const = 0;
+ // Retrieve LLSD corresponding to a "slice" of the result set: a
+ // contiguous sub-array starting at index. The returned LLSD array might
+ // be shorter than count entries if count > MAX_ITEM_LIMIT, or if the
+ // specified slice contains the end of the result set.
+ LLSD getSlice(int index, int count) const;
+ // Like getSlice(), but also return adjusted start position.
+ std::pair<LLSD, int> getSliceStart(int index, int count) const;
+/*==========================================================================*|
+ // Retrieve LLSD corresponding to a single entry from the result set,
+ // with index validation.
+ LLSD getSingle(int index) const;
+|*==========================================================================*/
+
+ /*---------------- the rest is solely for debug logging ----------------*/
+ std::string mName;
+
+ ResultSet(const std::string& name);
+ virtual ~ResultSet();
+};
+
+} // namespace LL
+
+std::ostream& operator<<(std::ostream& out, const LL::ResultSet& self);
+
+#endif /* ! defined(LL_RESULTSET_H) */
diff --git a/indra/llcommon/scope_exit.h b/indra/llcommon/scope_exit.h
new file mode 100644
index 0000000000..eb4df4f74d
--- /dev/null
+++ b/indra/llcommon/scope_exit.h
@@ -0,0 +1,34 @@
+/**
+ * @file scope_exit.h
+ * @author Nat Goodspeed
+ * @date 2024-08-15
+ * @brief Cheap imitation of std::experimental::scope_exit
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Copyright (c) 2024, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_SCOPE_EXIT_H)
+#define LL_SCOPE_EXIT_H
+
+#include <functional>
+
+namespace LL
+{
+
+class scope_exit
+{
+public:
+ scope_exit(const std::function<void()>& func): mFunc(func) {}
+ scope_exit(const scope_exit&) = delete;
+ scope_exit& operator=(const scope_exit&) = delete;
+ ~scope_exit() { mFunc(); }
+
+private:
+ std::function<void()> mFunc;
+};
+
+} // namespace LL
+
+#endif /* ! defined(LL_SCOPE_EXIT_H) */
diff --git a/indra/llcommon/stringize.h b/indra/llcommon/stringize.h
index 536a18abc1..2730728637 100644
--- a/indra/llcommon/stringize.h
+++ b/indra/llcommon/stringize.h
@@ -88,13 +88,13 @@ struct gstringize_impl
};
// partially specialize for a single STRING argument -
-// note that ll_convert<T>(T) already handles the trivial case
+// note that ll_convert_to<T>(T) already handles the trivial case
template <typename OUTCHAR, typename INCHAR>
struct gstringize_impl<OUTCHAR, std::basic_string<INCHAR>>
{
auto operator()(const std::basic_string<INCHAR>& arg)
{
- return ll_convert<std::basic_string<OUTCHAR>>(arg);
+ return ll_convert_to<std::basic_string<OUTCHAR>>(arg);
}
};
@@ -105,7 +105,7 @@ struct gstringize_impl<OUTCHAR, INCHAR*>
{
auto operator()(const INCHAR* arg)
{
- return ll_convert<std::basic_string<OUTCHAR>>(arg);
+ return ll_convert_to<std::basic_string<OUTCHAR>>(arg);
}
};
@@ -190,16 +190,4 @@ void destringize_f(std::basic_string<CHARTYPE> const & str, Functor const & f)
f(in);
}
-/**
- * DESTRINGIZE(str, item1 >> item2 >> item3 ...) effectively expands to the
- * following:
- * @code
- * std::istringstream in(str);
- * in >> item1 >> item2 >> item3 ... ;
- * @endcode
- */
-#define DESTRINGIZE(STR, EXPRESSION) (destringize_f((STR), [&](auto& in){in >> EXPRESSION;}))
-// legacy name, just use DESTRINGIZE() going forward
-#define DEWSTRINGIZE(STR, EXPRESSION) DESTRINGIZE(STR, EXPRESSION)
-
#endif /* ! defined(LL_STRINGIZE_H) */
diff --git a/indra/llcommon/tempset.h b/indra/llcommon/tempset.h
new file mode 100755
index 0000000000..773f19f756
--- /dev/null
+++ b/indra/llcommon/tempset.h
@@ -0,0 +1,41 @@
+/**
+ * @file tempset.h
+ * @author Nat Goodspeed
+ * @date 2024-06-12
+ * @brief Temporarily override a variable for scope duration, then restore
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Copyright (c) 2024, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_TEMPSET_H)
+#define LL_TEMPSET_H
+
+// RAII class to set specified variable to specified value
+// only for the duration of containing scope
+template <typename VAR, typename VALUE>
+class TempSet
+{
+public:
+ TempSet(VAR& var, const VALUE& value):
+ mVar(var),
+ mOldValue(mVar)
+ {
+ mVar = value;
+ }
+
+ TempSet(const TempSet&) = delete;
+ TempSet& operator=(const TempSet&) = delete;
+
+ ~TempSet()
+ {
+ mVar = mOldValue;
+ }
+
+private:
+ VAR& mVar;
+ VAR mOldValue;
+};
+
+#endif /* ! defined(LL_TEMPSET_H) */
diff --git a/indra/llcommon/tests/StringVec.h b/indra/llcommon/tests/StringVec.h
index 4311cba992..3f8665affb 100644
--- a/indra/llcommon/tests/StringVec.h
+++ b/indra/llcommon/tests/StringVec.h
@@ -18,6 +18,16 @@
typedef std::vector<std::string> StringVec;
+#if defined(LL_LLTUT_H)
+// Modern compilers require us to define operator<<(std::ostream&, StringVec)
+// before the definition of the ensure() template that engages it. The error
+// stating that the compiler can't find a viable operator<<() is so perplexing
+// that even though I've obviously hit it a couple times before, a new
+// instance still caused much head-scratching. This warning is intended to
+// demystify any inadvertent future recurrence.
+#warning "StringVec.h must be #included BEFORE lltut.h for ensure() to work"
+#endif
+
std::ostream& operator<<(std::ostream& out, const StringVec& strings)
{
out << '(';
diff --git a/indra/llcommon/tests/llcond_test.cpp b/indra/llcommon/tests/llcond_test.cpp
index f2a302ed13..7d1ac6edb9 100644
--- a/indra/llcommon/tests/llcond_test.cpp
+++ b/indra/llcommon/tests/llcond_test.cpp
@@ -19,6 +19,7 @@
// other Linden headers
#include "../test/lltut.h"
#include "llcoros.h"
+#include "lldefs.h" // llless()
/*****************************************************************************
* TUT
@@ -64,4 +65,78 @@ namespace tut
cond.set_all(2);
cond.wait_equal(3);
}
+
+ template <typename T0, typename T1>
+ struct compare
+ {
+ const char* desc;
+ T0 lhs;
+ T1 rhs;
+ bool expect;
+
+ void test() const
+ {
+ // fails
+// ensure_equals(desc, (lhs < rhs), expect);
+ ensure_equals(desc, llless(lhs, rhs), expect);
+ }
+ };
+
+ template<> template<>
+ void object::test<3>()
+ {
+ set_test_name("comparison");
+ // Try to construct signed and unsigned variables such that the
+ // compiler can't optimize away the code to compare at runtime.
+ std::istringstream input("-1 10 20 10 20");
+ int minus1, s10, s20;
+ input >> minus1 >> s10 >> s20;
+ unsigned u10, u20;
+ input >> u10 >> u20;
+ ensure_equals("minus1 wrong", minus1, -1);
+ ensure_equals("s10 wrong", s10, 10);
+ ensure_equals("s20 wrong", s20, 20);
+ ensure_equals("u10 wrong", u10, 10);
+ ensure_equals("u20 wrong", u20, 20);
+ // signed < signed should always work!
+ compare<int, int> ss[] =
+ { {"minus1 < s10", minus1, s10, true},
+ {"s10 < s10", s10, s10, false},
+ {"s20 < s10", s20, s20, false}
+ };
+ for (const auto& cmp : ss)
+ {
+ cmp.test();
+ }
+ // unsigned < unsigned should always work!
+ compare<unsigned, unsigned> uu[] =
+ { {"u10 < u20", u10, u20, true},
+ {"u20 < u20", u20, u20, false},
+ {"u20 < u10", u20, u10, false}
+ };
+ for (const auto& cmp : uu)
+ {
+ cmp.test();
+ }
+ // signed < unsigned ??
+ compare<int, unsigned> su[] =
+ { {"minus1 < u10", minus1, u10, true},
+ {"s10 < u10", s10, u10, false},
+ {"s20 < u10", s20, u10, false}
+ };
+ for (const auto& cmp : su)
+ {
+ cmp.test();
+ }
+ // unsigned < signed ??
+ compare<unsigned, int> us[] =
+ { {"u10 < minus1", u10, minus1, false},
+ {"u10 < s10", u10, s10, false},
+ {"u10 < s20", u10, s20, true}
+ };
+ for (const auto& cmp : us)
+ {
+ cmp.test();
+ }
+ }
} // namespace tut
diff --git a/indra/llcommon/tests/llerror_test.cpp b/indra/llcommon/tests/llerror_test.cpp
index 3ec429530c..d597e90ba0 100644
--- a/indra/llcommon/tests/llerror_test.cpp
+++ b/indra/llcommon/tests/llerror_test.cpp
@@ -112,10 +112,10 @@ namespace tut
mMessages.push_back(message);
}
- int countMessages() { return (int) mMessages.size(); }
+ int countMessages() const { return (int) mMessages.size(); }
void clearMessages() { mMessages.clear(); }
- std::string message(int n)
+ std::string message(int n) const
{
std::ostringstream test_name;
test_name << "testing message " << n << ", not enough messages";
@@ -124,6 +124,16 @@ namespace tut
return mMessages[n];
}
+ void reportMessages() const
+ {
+ std::cerr << '\n';
+ int n = 0;
+ for (const auto& msg : mMessages)
+ {
+ std::cerr << std::setw(2) << n++ << ": " << msg.substr(0, 100) << '\n';
+ }
+ }
+
private:
typedef std::vector<std::string> MessageVector;
MessageVector mMessages;
@@ -134,6 +144,8 @@ namespace tut
LLError::RecorderPtr mRecorder;
LLError::SettingsStoragePtr mPriorErrorSettings;
+ auto recorder() { return std::dynamic_pointer_cast<TestRecorder>(mRecorder); }
+
ErrorTestData():
mRecorder(new TestRecorder())
{
@@ -153,27 +165,32 @@ namespace tut
int countMessages()
{
- return std::dynamic_pointer_cast<TestRecorder>(mRecorder)->countMessages();
+ return recorder()->countMessages();
}
void clearMessages()
{
- std::dynamic_pointer_cast<TestRecorder>(mRecorder)->clearMessages();
+ recorder()->clearMessages();
}
void setWantsTime(bool t)
- {
- std::dynamic_pointer_cast<TestRecorder>(mRecorder)->showTime(t);
- }
+ {
+ recorder()->showTime(t);
+ }
void setWantsMultiline(bool t)
- {
- std::dynamic_pointer_cast<TestRecorder>(mRecorder)->showMultiline(t);
- }
+ {
+ recorder()->showMultiline(t);
+ }
std::string message(int n)
{
- return std::dynamic_pointer_cast<TestRecorder>(mRecorder)->message(n);
+ return recorder()->message(n);
+ }
+
+ void reportMessages()
+ {
+ recorder()->reportMessages();
}
void ensure_message_count(int expectedCount)
@@ -204,7 +221,7 @@ namespace tut
on_field++;
}
// except function, which may have embedded spaces so ends with " : "
- else if ( ( on_field == FUNCTION_FIELD )
+ else if (( on_field == FUNCTION_FIELD )
&& ( ':' == msg[scan+1] && ' ' == msg[scan+2] )
)
{
@@ -233,19 +250,35 @@ namespace tut
}
void ensure_message_field_equals(int msgnum, LogFieldIndex fieldnum, const std::string& expectedText)
- {
- std::ostringstream test_name;
- test_name << "testing message " << msgnum << " field " << FieldName[fieldnum] << "\n message: \"" << message(msgnum) << "\"\n ";
+ {
+ std::ostringstream test_name;
+ test_name << "testing message " << msgnum << " field " << FieldName[fieldnum] << "\n message: \"" << message(msgnum).substr(0, 100) << "\"\n ";
- ensure_equals(test_name.str(), message_field(msgnum, fieldnum), expectedText);
- }
+ try
+ {
+ ensure_equals(test_name.str(), message_field(msgnum, fieldnum), expectedText);
+ }
+ catch (const failure&)
+ {
+ reportMessages();
+ throw;
+ }
+ }
void ensure_message_does_not_contain(int n, const std::string& expectedText)
{
std::ostringstream test_name;
test_name << "testing message " << n;
- ensure_does_not_contain(test_name.str(), message(n), expectedText);
+ try
+ {
+ ensure_does_not_contain(test_name.str(), message(n), expectedText);
+ }
+ catch (const failure&)
+ {
+ reportMessages();
+ throw;
+ }
}
};
@@ -297,29 +330,33 @@ namespace tut
ensure_message_field_equals(3, MSG_FIELD, "four");
ensure_message_field_equals(3, LEVEL_FIELD, "ERROR");
ensure_message_field_equals(3, TAGS_FIELD, "#WriteTag#");
- ensure_message_count(4);
+ // LL_ERRS() produces 2 recordMessage() calls
+ ensure_message_count(5);
LLError::setDefaultLevel(LLError::LEVEL_INFO);
writeSome();
- ensure_message_field_equals(4, MSG_FIELD, "two");
- ensure_message_field_equals(5, MSG_FIELD, "three");
- ensure_message_field_equals(6, MSG_FIELD, "four");
- ensure_message_count(7);
+ ensure_message_field_equals(5, MSG_FIELD, "two");
+ ensure_message_field_equals(6, MSG_FIELD, "three");
+ ensure_message_field_equals(7, MSG_FIELD, "four");
+ // LL_ERRS() produces 2 recordMessage() calls
+ ensure_message_count(9);
LLError::setDefaultLevel(LLError::LEVEL_WARN);
writeSome();
- ensure_message_field_equals(7, MSG_FIELD, "three");
- ensure_message_field_equals(8, MSG_FIELD, "four");
- ensure_message_count(9);
+ ensure_message_field_equals(9, MSG_FIELD, "three");
+ ensure_message_field_equals(10, MSG_FIELD, "four");
+ // LL_ERRS() produces 2 recordMessage() calls
+ ensure_message_count(12);
LLError::setDefaultLevel(LLError::LEVEL_ERROR);
writeSome();
- ensure_message_field_equals(9, MSG_FIELD, "four");
- ensure_message_count(10);
+ ensure_message_field_equals(12, MSG_FIELD, "four");
+ // LL_ERRS() produces 2 recordMessage() calls
+ ensure_message_count(14);
LLError::setDefaultLevel(LLError::LEVEL_NONE);
writeSome();
- ensure_message_count(10);
+ ensure_message_count(14);
}
template<> template<>
@@ -331,7 +368,8 @@ namespace tut
ensure_message_field_equals(1, LEVEL_FIELD, "INFO");
ensure_message_field_equals(2, LEVEL_FIELD, "WARNING");
ensure_message_field_equals(3, LEVEL_FIELD, "ERROR");
- ensure_message_count(4);
+ // LL_ERRS() produces 2 recordMessage() calls
+ ensure_message_count(5);
}
template<> template<>
@@ -627,7 +665,8 @@ namespace tut
ensure_message_field_equals(0, LOCATION_FIELD, location);
ensure_message_field_equals(0, MSG_FIELD, "die");
- ensure_message_count(1);
+ // LL_ERRS() produces 2 recordMessage() calls
+ ensure_message_count(2);
ensure("fatal callback called", fatalWasCalled);
}
@@ -751,10 +790,12 @@ namespace tut
ensure_message_field_equals(0, MSG_FIELD, "aim west");
ensure_message_field_equals(1, MSG_FIELD, "ate eels");
- ensure_message_field_equals(2, MSG_FIELD, "buy iron");
- ensure_message_field_equals(3, MSG_FIELD, "bad word");
- ensure_message_field_equals(4, MSG_FIELD, "big easy");
- ensure_message_count(5);
+ // LL_ERRS() produces 2 recordMessage() calls
+ ensure_message_field_equals(3, MSG_FIELD, "buy iron");
+ ensure_message_field_equals(4, MSG_FIELD, "bad word");
+ ensure_message_field_equals(5, MSG_FIELD, "big easy");
+ // LL_ERRS() produces 2 recordMessage() calls
+ ensure_message_count(7);
}
template<> template<>
@@ -868,9 +909,11 @@ namespace tut
TestBeta::doAll();
ensure_message_field_equals(3, MSG_FIELD, "aim west");
ensure_message_field_equals(4, MSG_FIELD, "ate eels");
- ensure_message_field_equals(5, MSG_FIELD, "bad word");
- ensure_message_field_equals(6, MSG_FIELD, "big easy");
- ensure_message_count(7);
+ // LL_ERRS() produces 2 recordMessage() calls
+ ensure_message_field_equals(6, MSG_FIELD, "bad word");
+ ensure_message_field_equals(7, MSG_FIELD, "big easy");
+ // LL_ERRS() produces 2 recordMessage() calls
+ ensure_message_count(9);
}
}
diff --git a/indra/llcommon/tests/lleventcoro_test.cpp b/indra/llcommon/tests/lleventcoro_test.cpp
index ab174a8bde..0e352bce0f 100644
--- a/indra/llcommon/tests/lleventcoro_test.cpp
+++ b/indra/llcommon/tests/lleventcoro_test.cpp
@@ -111,14 +111,13 @@ namespace tut
void test_data::explicit_wait(std::shared_ptr<LLCoros::Promise<std::string>>& cbp)
{
- BEGIN
- {
- mSync.bump();
- // The point of this test is to verify / illustrate suspending a
- // coroutine for something other than an LLEventPump. In other
- // words, this shows how to adapt to any async operation that
- // provides a callback-style notification (and prove that it
- // works).
+ DEBUG;
+ mSync.bump();
+ // The point of this test is to verify / illustrate suspending a
+ // coroutine for something other than an LLEventPump. In other
+ // words, this shows how to adapt to any async operation that
+ // provides a callback-style notification (and prove that it
+ // works).
// Perhaps we would send a request to a remote server and arrange
// for cbp->set_value() to be called on response.
@@ -128,13 +127,11 @@ namespace tut
cbp = std::make_shared<LLCoros::Promise<std::string>>();
LLCoros::Future<std::string> future = LLCoros::getFuture(*cbp);
- // calling get() on the future causes us to suspend
- debug("about to suspend");
- stringdata = future.get();
- mSync.bump();
- ensure_equals("Got it", stringdata, "received");
- }
- END
+ // calling get() on the future causes us to suspend
+ debug("about to suspend");
+ stringdata = future.get();
+ mSync.bump();
+ ensure_equals("Got it", stringdata, "received");
}
template<> template<>
@@ -161,13 +158,9 @@ namespace tut
void test_data::waitForEventOn1()
{
- BEGIN
- {
- mSync.bump();
- result = suspendUntilEventOn("source");
- mSync.bump();
- }
- END
+ mSync.bump();
+ result = suspendUntilEventOn("source");
+ mSync.bump();
}
template<> template<>
@@ -187,15 +180,11 @@ namespace tut
void test_data::coroPump()
{
- BEGIN
- {
- mSync.bump();
- LLCoroEventPump waiter;
- replyName = waiter.getName();
- result = waiter.suspend();
- mSync.bump();
- }
- END
+ mSync.bump();
+ LLCoroEventPump waiter;
+ replyName = waiter.getName();
+ result = waiter.suspend();
+ mSync.bump();
}
template<> template<>
@@ -215,16 +204,12 @@ namespace tut
void test_data::postAndWait1()
{
- BEGIN
- {
- mSync.bump();
- result = postAndSuspend(LLSDMap("value", 17), // request event
- immediateAPI.getPump(), // requestPump
- "reply1", // replyPump
- "reply"); // request["reply"] = name
- mSync.bump();
- }
- END
+ mSync.bump();
+ result = postAndSuspend(LLSDMap("value", 17), // request event
+ immediateAPI.getPump(), // requestPump
+ "reply1", // replyPump
+ "reply"); // request["reply"] = name
+ mSync.bump();
}
template<> template<>
@@ -238,15 +223,11 @@ namespace tut
void test_data::coroPumpPost()
{
- BEGIN
- {
- mSync.bump();
- LLCoroEventPump waiter;
- result = waiter.postAndSuspend(LLSDMap("value", 17),
- immediateAPI.getPump(), "reply");
- mSync.bump();
- }
- END
+ mSync.bump();
+ LLCoroEventPump waiter;
+ result = waiter.postAndSuspend(LLSDMap("value", 17),
+ immediateAPI.getPump(), "reply");
+ mSync.bump();
}
template<> template<>
diff --git a/indra/llcommon/tests/lleventdispatcher_test.cpp b/indra/llcommon/tests/lleventdispatcher_test.cpp
index 44f772e322..be05ca4e85 100644
--- a/indra/llcommon/tests/lleventdispatcher_test.cpp
+++ b/indra/llcommon/tests/lleventdispatcher_test.cpp
@@ -17,13 +17,13 @@
// std headers
// external library headers
// other Linden headers
+#include "StringVec.h"
#include "../test/lltut.h"
#include "lleventfilter.h"
#include "llsd.h"
#include "llsdutil.h"
#include "llevents.h"
#include "stringize.h"
-#include "StringVec.h"
#include "tests/wrapllerrs.h"
#include "../test/catch_and_store_what_in.h"
#include "../test/debug.h"
diff --git a/indra/llcommon/tests/lleventfilter_test.cpp b/indra/llcommon/tests/lleventfilter_test.cpp
index d7b80e2545..9ca1ca4e2e 100644
--- a/indra/llcommon/tests/lleventfilter_test.cpp
+++ b/indra/llcommon/tests/lleventfilter_test.cpp
@@ -34,10 +34,10 @@
// std headers
// external library headers
// other Linden headers
+#include "listener.h"
#include "../test/lltut.h"
#include "stringize.h"
#include "llsdutil.h"
-#include "listener.h"
#include "tests/wrapllerrs.h"
#include <typeinfo>
@@ -51,6 +51,7 @@
// 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.
+#if 0 // time testing needs reworking
class TestEventTimeout: public LLEventTimeoutBase
{
public:
@@ -151,6 +152,7 @@ public:
F32 mAlarmRemaining, mTimerRemaining;
LLEventTimeoutBase::Action mAlarmAction;
};
+#endif // time testing needs reworking
/*****************************************************************************
* TUT
@@ -220,6 +222,8 @@ namespace tut
void filter_object::test<2>()
{
set_test_name("LLEventTimeout::actionAfter()");
+ skip("time testing needs reworking");
+#if 0 // time testing needs reworking
LLEventPump& driver(pumps.obtain("driver"));
TestEventTimeout filter(driver);
listener0.reset(0);
@@ -285,12 +289,15 @@ namespace tut
filter.forceTimeout();
mainloop.post(17);
check_listener("no timeout 6", listener1, LLSD(0));
+#endif // time testing needs reworking
}
template<> template<>
void filter_object::test<3>()
{
set_test_name("LLEventTimeout::eventAfter()");
+ skip("time testing needs reworking");
+#if 0 // time testing needs reworking
LLEventPump& driver(pumps.obtain("driver"));
TestEventTimeout filter(driver);
listener0.reset(0);
@@ -322,12 +329,15 @@ namespace tut
filter.forceTimeout();
mainloop.post(17);
check_listener("no timeout 3", listener0, LLSD(0));
+#endif // time testing needs reworking
}
template<> template<>
void filter_object::test<4>()
{
set_test_name("LLEventTimeout::errorAfter()");
+ skip("time testing needs reworking");
+#if 0 // time testing needs reworking
WrapLLErrs capture;
LLEventPump& driver(pumps.obtain("driver"));
TestEventTimeout filter(driver);
@@ -362,12 +372,15 @@ namespace tut
filter.forceTimeout();
mainloop.post(17);
check_listener("no timeout 3", listener0, LLSD(0));
+#endif // time testing needs reworking
}
template<> template<>
void filter_object::test<5>()
{
set_test_name("LLEventThrottle");
+ skip("time testing needs reworking");
+#if 0 // time testing needs reworking
TestEventThrottle throttle(3);
Concat cat;
throttle.listen("concat", boost::ref(cat));
@@ -403,6 +416,7 @@ namespace tut
throttle.advance(5);
throttle.post(";17");
ensure_equals("17", cat.result, "136;12;17"); // "17" delivered
+#endif // time testing needs reworking
}
template<class PUMP>
diff --git a/indra/llcommon/tests/llleap_test.cpp b/indra/llcommon/tests/llleap_test.cpp
index fa48bcdefd..ca1939c81e 100644
--- a/indra/llcommon/tests/llleap_test.cpp
+++ b/indra/llcommon/tests/llleap_test.cpp
@@ -18,6 +18,7 @@
#include <functional>
// external library headers
// other Linden headers
+#include "StringVec.h"
#include "../test/lltut.h"
#include "../test/namedtempfile.h"
#include "../test/catch_and_store_what_in.h"
@@ -26,7 +27,6 @@
#include "llprocess.h"
#include "llstring.h"
#include "stringize.h"
-#include "StringVec.h"
#if defined(LL_WINDOWS)
#define sleep(secs) _sleep((secs) * 1000)
@@ -385,8 +385,7 @@ namespace tut
"result = '' if resp == dict(pump=replypump(), data='ack')\\\n"
" else 'bad: ' + str(resp)\n"
"send(pump='" << result.getName() << "', data=result)\n";});
- waitfor(LLLeap::create(get_test_name(),
- StringVec{PYTHON, script.getName()}));
+ waitfor(LLLeap::create(get_test_name(), StringVec{PYTHON, script.getName()}));
result.ensure();
}
diff --git a/indra/llcommon/tests/llmainthreadtask_test.cpp b/indra/llcommon/tests/llmainthreadtask_test.cpp
index 9ccf391327..ea4232ad78 100644
--- a/indra/llcommon/tests/llmainthreadtask_test.cpp
+++ b/indra/llcommon/tests/llmainthreadtask_test.cpp
@@ -20,8 +20,8 @@
// other Linden headers
#include "../test/lltut.h"
#include "../test/sync.h"
+#include "llcallbacklist.h"
#include "llthread.h" // on_main_thread()
-#include "lleventtimer.h"
#include "lockstatic.h"
/*****************************************************************************
@@ -108,7 +108,7 @@ namespace tut
lk.unlock();
// run the task -- should unblock thread, which will immediately block
// on mSync
- LLEventTimer::updateClass();
+ LLCallbackList::instance().callFunctions();
// 'lk', having unlocked, can no longer be used to access; relock with
// a new LockStatic instance
ensure("should now have run", LockStatic()->ran);
diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp
index 6e8422ca0c..9b3b345217 100644
--- a/indra/llcommon/tests/llprocess_test.cpp
+++ b/indra/llcommon/tests/llprocess_test.cpp
@@ -259,6 +259,7 @@ public:
}
std::string getName() const { return mPath.string(); }
+ std::string getNormalName() const { return mPath.lexically_normal().make_preferred().string(); }
private:
boost::filesystem::path mPath;
@@ -590,7 +591,7 @@ namespace tut
" f.write(os.path.normcase(os.path.normpath(os.getcwd())))\n");
// Before running, call setWorkingDirectory()
py.mParams.cwd = tempdir.getName();
- std::string expected{ tempdir.getName() };
+ std::string expected{ tempdir.getNormalName() };
#if LL_WINDOWS
// SIGH, don't get tripped up by "C:" != "c:" --
// but on the Mac, using tolower() fails because "/users" != "/Users"!
diff --git a/indra/llcommon/tests/llrand_test.cpp b/indra/llcommon/tests/llrand_test.cpp
index a0dd4ef576..85dd53ce96 100644
--- a/indra/llcommon/tests/llrand_test.cpp
+++ b/indra/llcommon/tests/llrand_test.cpp
@@ -38,7 +38,7 @@
// testing extent < 0, negate the return value and the extent before passing
// into ensure_in_range().
template <typename NUMBER>
-void ensure_in_range(const std::string_view& name,
+void ensure_in_range(std::string_view name,
NUMBER value, NUMBER low, NUMBER high)
{
auto failmsg{ stringize(name, " >= ", low, " (", value, ')') };
diff --git a/indra/llcommon/tests/llsdserialize_test.cpp b/indra/llcommon/tests/llsdserialize_test.cpp
index fae9f7023f..ff0607f503 100644
--- a/indra/llcommon/tests/llsdserialize_test.cpp
+++ b/indra/llcommon/tests/llsdserialize_test.cpp
@@ -44,19 +44,17 @@ typedef U32 uint32_t;
#include "llstring.h"
#endif
-#include "boost/range.hpp"
-
#include "llsd.h"
#include "llsdserialize.h"
#include "llsdutil.h"
#include "llformat.h"
#include "llmemorystream.h"
-#include "../test/hexdump.h"
+#include "hexdump.h"
+#include "StringVec.h"
#include "../test/lltut.h"
#include "../test/namedtempfile.h"
#include "stringize.h"
-#include "StringVec.h"
#include <functional>
typedef std::function<void(const LLSD& data, std::ostream& str)> FormatterFunction;
@@ -1921,12 +1919,12 @@ namespace tut
int bufflen{ static_cast<int>(buffstr.length()) };
out.write(reinterpret_cast<const char*>(&bufflen), sizeof(bufflen));
LL_DEBUGS() << "Wrote length: "
- << hexdump(reinterpret_cast<const char*>(&bufflen),
- sizeof(bufflen))
+ << LL::hexdump(reinterpret_cast<const char*>(&bufflen),
+ sizeof(bufflen))
<< LL_ENDL;
out.write(buffstr.c_str(), buffstr.length());
LL_DEBUGS() << "Wrote data: "
- << hexmix(buffstr.c_str(), buffstr.length())
+ << LL::hexmix(buffstr.c_str(), buffstr.length())
<< LL_ENDL;
}
}
@@ -2097,7 +2095,8 @@ namespace tut
NamedTempFile file("llsd", "");
python("Python " + pyformatter,
- [&](std::ostream& out){ out <<
+ [pyformatter, &file](std::ostream& out) {
+ out <<
import_llsd <<
"import struct\n"
"lenformat = struct.Struct('i')\n"
diff --git a/indra/llcommon/threadpool.cpp b/indra/llcommon/threadpool.cpp
index 451e60c083..c94b5eecce 100644
--- a/indra/llcommon/threadpool.cpp
+++ b/indra/llcommon/threadpool.cpp
@@ -18,6 +18,7 @@
// external library headers
// other Linden headers
#include "commoncontrol.h"
+#include "llcoros.h"
#include "llerror.h"
#include "llevents.h"
#include "llsd.h"
@@ -90,20 +91,14 @@ void LL::ThreadPoolBase::start()
return;
}
- // Listen on "LLApp", and when the app is shutting down, close the queue
- // and join the workers.
- LLEventPumps::instance().obtain("LLApp").listen(
+ // When the app is shutting down, close the queue and join the workers.
+ mStopListener = LLCoros::getStopListener(
mName,
- [this](const LLSD& stat)
+ [this](const LLSD& status)
{
- std::string status(stat["status"]);
- if (status != "running")
- {
- // viewer is starting shutdown -- proclaim the end is nigh!
- LL_DEBUGS("ThreadPool") << mName << " saw " << status << LL_ENDL;
- close();
- }
- return false;
+ // viewer is starting shutdown -- proclaim the end is nigh!
+ LL_DEBUGS("ThreadPool") << mName << " saw " << status << LL_ENDL;
+ close();
});
}
@@ -118,20 +113,19 @@ LL::ThreadPoolBase::~ThreadPoolBase()
void LL::ThreadPoolBase::close()
{
- if (! mQueue->isClosed())
+ // mQueue might have been closed already, but in any case we must join or
+ // detach each of our threads before destroying the mThreads vector.
+ LL_DEBUGS("ThreadPool") << mName << " closing queue and joining threads" << LL_ENDL;
+ mQueue->close();
+ for (auto& pair: mThreads)
{
- LL_DEBUGS("ThreadPool") << mName << " closing queue and joining threads" << LL_ENDL;
- mQueue->close();
- for (auto& pair: mThreads)
+ if (pair.second.joinable())
{
- if (pair.second.joinable())
- {
- LL_DEBUGS("ThreadPool") << mName << " waiting on thread " << pair.first << LL_ENDL;
- pair.second.join();
- }
+ LL_DEBUGS("ThreadPool") << mName << " waiting on thread " << pair.first << LL_ENDL;
+ pair.second.join();
}
- LL_DEBUGS("ThreadPool") << mName << " shutdown complete" << LL_ENDL;
}
+ LL_DEBUGS("ThreadPool") << mName << " shutdown complete" << LL_ENDL;
}
void LL::ThreadPoolBase::run(const std::string& name)
diff --git a/indra/llcommon/threadpool.h b/indra/llcommon/threadpool.h
index 0eb1891754..2748d7b073 100644
--- a/indra/llcommon/threadpool.h
+++ b/indra/llcommon/threadpool.h
@@ -13,6 +13,7 @@
#if ! defined(LL_THREADPOOL_H)
#define LL_THREADPOOL_H
+#include "llcoros.h"
#include "threadpool_fwd.h"
#include "workqueue.h"
#include <memory> // std::unique_ptr
@@ -52,8 +53,8 @@ namespace LL
void start();
/**
- * ThreadPool listens for application shutdown messages on the "LLApp"
- * LLEventPump. Call close() to shut down this ThreadPool early.
+ * ThreadPool listens for application shutdown events. Call close() to
+ * shut down this ThreadPool early.
*/
virtual void close();
@@ -95,6 +96,7 @@ namespace LL
std::string mName;
size_t mThreadCount;
+ LLTempBoundListener mStopListener;
};
/**
diff --git a/indra/llcommon/throttle.cpp b/indra/llcommon/throttle.cpp
new file mode 100644
index 0000000000..deb0a26fb0
--- /dev/null
+++ b/indra/llcommon/throttle.cpp
@@ -0,0 +1,34 @@
+/**
+ * @file throttle.cpp
+ * @author Nat Goodspeed
+ * @date 2024-08-12
+ * @brief Implementation for ThrottleBase.
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Copyright (c) 2024, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "throttle.h"
+// STL headers
+// std headers
+// external library headers
+// other Linden headers
+#include "lltimer.h"
+
+bool ThrottleBase::too_fast()
+{
+ F64 now = LLTimer::getElapsedSeconds();
+ if (now < mNext)
+ {
+ return true;
+ }
+ else
+ {
+ mNext = now + mInterval;
+ return false;
+ }
+}
diff --git a/indra/llcommon/throttle.h b/indra/llcommon/throttle.h
new file mode 100644
index 0000000000..7f5389f464
--- /dev/null
+++ b/indra/llcommon/throttle.h
@@ -0,0 +1,138 @@
+/**
+ * @file throttle.h
+ * @author Nat Goodspeed
+ * @date 2024-08-12
+ * @brief Throttle class
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Copyright (c) 2024, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_THROTTLE_H)
+#define LL_THROTTLE_H
+
+#include "apply.h" // LL::bind_front()
+#include "llerror.h"
+#include <functional>
+#include <iomanip> // std::quoted()
+
+class ThrottleBase
+{
+public:
+ ThrottleBase(F64 interval):
+ mInterval(interval)
+ {}
+
+protected:
+ bool too_fast(); // not const: we update mNext
+ F64 mInterval, mNext{ 0. };
+};
+
+/**
+ * An instance of Throttle mediates calls to some other specified function,
+ * ensuring that it's called no more often than the specified time interval.
+ * Throttle is an abstract base class that delegates the behavior when the
+ * specified interval is exceeded.
+ */
+template <typename SIGNATURE>
+class Throttle: public ThrottleBase
+{
+public:
+ Throttle(const std::string& desc,
+ const std::function<SIGNATURE>& func,
+ F64 interval):
+ ThrottleBase(interval),
+ mDesc(desc),
+ mFunc(func)
+ {}
+ // Constructing Throttle with a member function pointer but without an
+ // instance pointer requires you to pass the instance pointer/reference as
+ // the first argument to operator()().
+ template <typename R, class C>
+ Throttle(const std::string& desc, R C::* method, F64 interval):
+ Throttle(desc, std::mem_fn(method), interval)
+ {}
+ template <typename R, class C>
+ Throttle(const std::string& desc, R C::* method, C* instance, F64 interval):
+ Throttle(desc, LL::bind_front(method, instance), interval)
+ {}
+ template <typename R, class C>
+ Throttle(const std::string& desc, R C::* method, const C* instance, F64 interval):
+ Throttle(desc, LL::bind_front(method, instance), interval)
+ {}
+ virtual ~Throttle() {}
+
+ template <typename... ARGS>
+ auto operator()(ARGS... args)
+ {
+ if (too_fast())
+ {
+ suppress();
+ using rtype = decltype(mFunc(std::forward<ARGS>(args)...));
+ if constexpr (! std::is_same_v<rtype, void>)
+ {
+ return rtype{};
+ }
+ }
+ else
+ {
+ return mFunc(std::forward<ARGS>(args)...);
+ }
+ }
+
+protected:
+ // override with desired behavior when calls come too often
+ virtual void suppress() = 0;
+ const std::string mDesc;
+
+private:
+ std::function<SIGNATURE> mFunc;
+};
+
+/**
+ * An instance of LogThrottle mediates calls to some other specified function,
+ * ensuring that it's called no more often than the specified time interval.
+ * When that interval is exceeded, it logs a message at the specified log
+ * level. It uses LL_MUMBLES_ONCE() logic to prevent spamming, since a too-
+ * frequent call may well be spammy.
+ */
+template <LLError::ELevel LOGLEVEL, typename SIGNATURE>
+class LogThrottle: public Throttle<SIGNATURE>
+{
+ using super = Throttle<SIGNATURE>;
+public:
+ LogThrottle(const std::string& desc,
+ const std::function<SIGNATURE>& func,
+ F64 interval):
+ super(desc, func, interval)
+ {}
+ template <typename R, class C>
+ LogThrottle(const std::string& desc, R C::* method, F64 interval):
+ super(desc, method, interval)
+ {}
+ template <typename R, class C>
+ LogThrottle(const std::string& desc, R C::* method, C* instance, F64 interval):
+ super(desc, method, instance, interval)
+ {}
+ template <typename R, class C>
+ LogThrottle(const std::string& desc, R C::* method, const C* instance, F64 interval):
+ super(desc, method, instance, interval)
+ {}
+
+private:
+ void suppress() override
+ {
+ // Using lllog(), the macro underlying LL_WARNS() et al., allows
+ // specifying compile-time LOGLEVEL. It does NOT support a variable
+ // LOGLEVEL, which is why LOGLEVEL is a non-type template parameter.
+ // See llvlog() for variable support, which is a bit more expensive.
+ // true = only print the log message once
+ lllog(LOGLEVEL, true, "LogThrottle") << std::quoted(super::mDesc)
+ << " called more than once per "
+ << super::mInterval
+ << LL_ENDL;
+ }
+};
+
+#endif /* ! defined(LL_THROTTLE_H) */
diff --git a/indra/llcommon/workqueue.cpp b/indra/llcommon/workqueue.cpp
index 6066e74fb5..9138c862f9 100644
--- a/indra/llcommon/workqueue.cpp
+++ b/indra/llcommon/workqueue.cpp
@@ -32,8 +32,11 @@ using Lock = LLCoros::LockType;
LL::WorkQueueBase::WorkQueueBase(const std::string& name):
super(makeName(name))
{
- // TODO: register for "LLApp" events so we can implicitly close() on
- // viewer shutdown.
+ // Register for status change events so we'll implicitly close() on viewer
+ // shutdown.
+ mStopListener = LLCoros::getStopListener(
+ "WorkQueue:" + getKey(),
+ [this](const LLSD&){ close(); });
}
void LL::WorkQueueBase::runUntilClose()
diff --git a/indra/llcommon/workqueue.h b/indra/llcommon/workqueue.h
index 9d7bbfbf7a..eb6923df0b 100644
--- a/indra/llcommon/workqueue.h
+++ b/indra/llcommon/workqueue.h
@@ -13,6 +13,7 @@
#define LL_WORKQUEUE_H
#include "llcoros.h"
+#include "llevents.h"
#include "llexception.h"
#include "llinstancetracker.h"
#include "llinstancetrackersubclass.h"
@@ -116,16 +117,29 @@ namespace LL
ARGS&&... args);
/**
- * Post work to another WorkQueue, blocking the calling coroutine
- * until then, returning the result to caller on completion. Optional
- * final argument is TimePoint for WorkSchedule.
+ * Post work, blocking the calling coroutine, returning the result to
+ * caller on completion. Optional final argument is TimePoint for
+ * WorkSchedule.
*
* In general, we assume that each thread's default coroutine is busy
* servicing its WorkQueue or whatever. To try to prevent mistakes, we
* forbid calling waitForResult() from a thread's default coroutine.
*/
template <typename CALLABLE, typename... ARGS>
- auto waitForResult(CALLABLE&& callable, ARGS&&... args);
+ auto waitForResult(CALLABLE&& callable, ARGS&&... args)
+ {
+ checkCoroutine("waitForResult()");
+ return waitForResult_(std::forward<CALLABLE>(callable),
+ std::forward<ARGS>(args)...);
+ }
+
+ /**
+ * Post work, blocking the calling coroutine, returning the result to
+ * caller on completion. Optional final argument is TimePoint for
+ * WorkSchedule.
+ */
+ template <typename CALLABLE, typename... ARGS>
+ auto waitForResult_(CALLABLE&& callable, ARGS&&... args);
/*--------------------------- worker API ---------------------------*/
@@ -194,6 +208,8 @@ namespace LL
static std::string makeName(const std::string& name);
void callWork(const Work& work);
+ LLTempBoundListener mStopListener;
+
private:
virtual Work pop_() = 0;
virtual bool tryPop_(Work&) = 0;
@@ -359,6 +375,10 @@ namespace LL
* CALLABLE that returns bool, a TimePoint and an interval at which to
* relaunch it. As long as the callable continues returning true, BackJack
* keeps resubmitting it to the target WorkQueue.
+ *
+ * "You go back, Jack, and do it again -- wheel turnin' round and round..."
+ * --Steely Dan, from "Can't Buy a Thrill" (1972)
+ * https://www.youtube.com/watch?v=yCgHTmv4YU8
*/
// Why is BackJack a class and not a lambda? Because, unlike a lambda, a
// class method gets its own 'this' pointer -- which we need to resubmit
@@ -525,7 +545,7 @@ namespace LL
reply,
// Bind the current exception to transport back to the
// originating WorkQueue. Once there, rethrow it.
- [exc = std::current_exception()](){ std::rethrow_exception(exc); });
+ [exc = std::current_exception()]{ std::rethrow_exception(exc); });
}
},
// if caller passed a TimePoint, pass it along to post()
@@ -618,9 +638,8 @@ namespace LL
};
template <typename CALLABLE, typename... ARGS>
- auto WorkQueueBase::waitForResult(CALLABLE&& callable, ARGS&&... args)
+ auto WorkQueueBase::waitForResult_(CALLABLE&& callable, ARGS&&... args)
{
- checkCoroutine("waitForResult()");
// derive callable's return type so we can specialize for void
return WaitForResult<CALLABLE, decltype(std::forward<CALLABLE>(callable)())>()
(this, std::forward<CALLABLE>(callable), std::forward<ARGS>(args)...);