diff options
author | Nat Goodspeed <nat@lindenlab.com> | 2024-09-25 11:56:44 -0400 |
---|---|---|
committer | Nat Goodspeed <nat@lindenlab.com> | 2024-09-25 11:56:44 -0400 |
commit | 55df7328c6f8c864ea309c57d73e791e079b3c2c (patch) | |
tree | cb4cba5e51d9649ba229c4eefeaafe783b9250e2 /indra/llcommon | |
parent | 4b2b94f4864f2e2e7d76f4f17b2d58bb959b3edb (diff) | |
parent | 86d2fb93b73d2689104c564ec859be7f83416691 (diff) |
Merge branch 'develop' into marchcat/xcode-16
Diffstat (limited to 'indra/llcommon')
90 files changed, 6197 insertions, 1348 deletions
diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index 60549d9d11..78bfaade55 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,14 @@ set(llcommon_SOURCE_FILES lluriparser.cpp lluuid.cpp llworkerthread.cpp - hbxxh.cpp - u64.cpp + lockstatic.cpp + lua_function.cpp + lualistener.cpp + resultset.cpp + scriptcommand.cpp threadpool.cpp + throttle.cpp + u64.cpp workqueue.cpp StackWalker.cpp ) @@ -117,9 +124,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 @@ -138,6 +148,7 @@ set(llcommon_HEADER_FILES llcommonutils.h llcond.h llcoros.h + llcoromutex.h llcrc.h llcriticaldamp.h lldate.h @@ -154,9 +165,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 @@ -173,6 +184,7 @@ set(llcommon_HEADER_FILES llinitparam.h llinstancetracker.h llinstancetrackersubclass.h + llinttracker.h llkeybind.h llkeythrottle.h llleap.h @@ -183,14 +195,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 @@ -198,6 +208,8 @@ set(llcommon_HEADER_FILES llprocess.h llprocessor.h llprocinfo.h + llprofiler.h + llprofilercategories.h llptrto.h llqueuedthread.h llrand.h @@ -215,14 +227,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 @@ -241,13 +253,18 @@ set(llcommon_HEADER_FILES lluuid.h llwin32headers.h llworkerthread.h - hbxxh.h lockstatic.h + lua_function.h + lualistener.h + resultset.h + scriptcommand.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/always_return.h b/indra/llcommon/always_return.h index a206471da5..a56a8b443e 100644 --- a/indra/llcommon/always_return.h +++ b/indra/llcommon/always_return.h @@ -14,6 +14,7 @@ #define LL_ALWAYS_RETURN_H #include <type_traits> // std::enable_if, std::is_convertible +#include <utility> // std::forward namespace LL { @@ -79,6 +80,22 @@ namespace LL DESIRED mDefault; }; + // specialize for AlwaysReturn<void> + template <> + struct AlwaysReturn<void> + { + public: + AlwaysReturn() {} + + // callable returns a type not convertible to DESIRED, return default + template <typename CALLABLE, typename... ARGS> + void operator()(CALLABLE&& callable, ARGS&&... args) + { + // discard whatever callable(args) returns + std::forward<CALLABLE>(callable)(std::forward<ARGS>(args)...); + } + }; + /** * always_return<T>(some_function, some_args...) calls * some_function(some_args...). It is guaranteed to return a value of type 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/fsyspath.h b/indra/llcommon/fsyspath.h new file mode 100644 index 0000000000..1b4aec09b4 --- /dev/null +++ b/indra/llcommon/fsyspath.h @@ -0,0 +1,79 @@ +/** + * @file fsyspath.h + * @author Nat Goodspeed + * @date 2024-04-03 + * @brief Adapt our UTF-8 std::strings for std::filesystem::path + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_FSYSPATH_H) +#define LL_FSYSPATH_H + +#include <filesystem> + +// While std::filesystem::path can be directly constructed from std::string on +// both Posix and Windows, that's not what we want on Windows. Per +// https://en.cppreference.com/w/cpp/filesystem/path/path: + +// ... the method of conversion to the native character set depends on the +// character type used by source. +// +// * If the source character type is char, the encoding of the source is +// assumed to be the native narrow encoding (so no conversion takes place on +// POSIX systems). +// * If the source character type is char8_t, conversion from UTF-8 to native +// filesystem encoding is used. (since C++20) +// * If the source character type is wchar_t, the input is assumed to be the +// native wide encoding (so no conversion takes places on Windows). + +// The trouble is that on Windows, from std::string ("source character type is +// char"), the "native narrow encoding" isn't UTF-8, so file paths containing +// non-ASCII characters get mangled. +// +// Once we're building with C++20, we could pass a UTF-8 std::string through a +// vector<char8_t> to engage std::filesystem::path's own UTF-8 conversion. But +// sigh, as of 2024-04-03 we're not yet there. +// +// Anyway, encapsulating the important UTF-8 conversions in our own subclass +// allows us to migrate forward to C++20 conventions without changing +// referencing code. + +class fsyspath: public std::filesystem::path +{ + using super = std::filesystem::path; + +public: + // default + fsyspath() {} + // construct from UTF-8 encoded std::string + fsyspath(const std::string& path): super(std::filesystem::u8path(path)) {} + // construct from UTF-8 encoded const char* + fsyspath(const char* path): super(std::filesystem::u8path(path)) {} + // construct from existing path + fsyspath(const super& path): super(path) {} + + fsyspath& operator=(const super& p) { super::operator=(p); return *this; } + fsyspath& operator=(const std::string& p) + { + super::operator=(std::filesystem::u8path(p)); + return *this; + } + fsyspath& operator=(const char* p) + { + super::operator=(std::filesystem::u8path(p)); + return *this; + } + + // shadow base-class string() method with UTF-8 aware method + std::string string() const { return super::u8string(); } + // On Posix systems, where value_type is already char, this operator + // std::string() method shadows the base class operator string_type() + // method. But on Windows, where value_type is wchar_t, the base class + // doesn't have operator std::string(). Provide it. + operator std::string() const { return string(); } +}; + +#endif /* ! defined(LL_FSYSPATH_H) */ diff --git a/indra/llcommon/hexdump.h b/indra/llcommon/hexdump.h new file mode 100755 index 0000000000..4b734426a3 --- /dev/null +++ b/indra/llcommon/hexdump.h @@ -0,0 +1,106 @@ +/** + * @file hexdump.h + * @author Nat Goodspeed + * @date 2023-10-03 + * @brief iostream manipulators to stream hex, or string with nonprinting chars + * + * $LicenseInfo:firstyear=2023&license=viewerlgpl$ + * Copyright (c) 2023, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_HEXDUMP_H) +#define LL_HEXDUMP_H + +#include <cctype> +#include <iomanip> +#include <iostream> +#include <string_view> + +namespace LL +{ + +// Format a given byte string as 2-digit hex values, no separators +// Usage: std::cout << hexdump(somestring) << ... +class hexdump +{ +public: + hexdump(std::string_view data): + hexdump(data.data(), data.length()) + {} + + hexdump(const char* data, size_t len): + hexdump(reinterpret_cast<const unsigned char*>(data), len) + {} + + hexdump(const std::vector<unsigned char>& data): + hexdump(data.data(), data.size()) + {} + + hexdump(const unsigned char* data, size_t len): + mData(data, data + len) + {} + + friend std::ostream& operator<<(std::ostream& out, const hexdump& self) + { + auto oldfmt{ out.flags() }; + auto oldfill{ out.fill() }; + out.setf(std::ios_base::hex, std::ios_base::basefield); + out.fill('0'); + for (auto c : self.mData) + { + out << std::setw(2) << unsigned(c); + } + out.setf(oldfmt, std::ios_base::basefield); + out.fill(oldfill); + return out; + } + +private: + std::vector<unsigned char> mData; +}; + +// Format a given byte string as a mix of printable characters and, for each +// non-printable character, "\xnn" +// Usage: std::cout << hexmix(somestring) << ... +class hexmix +{ +public: + hexmix(std::string_view data): + mData(data) + {} + + hexmix(const char* data, size_t len): + mData(data, len) + {} + + friend std::ostream& operator<<(std::ostream& out, const hexmix& self) + { + auto oldfmt{ out.flags() }; + auto oldfill{ out.fill() }; + out.setf(std::ios_base::hex, std::ios_base::basefield); + out.fill('0'); + for (auto c : self.mData) + { + // std::isprint() must be passed an unsigned char! + if (std::isprint(static_cast<unsigned char>(c))) + { + out << c; + } + else + { + out << "\\x" << std::setw(2) << unsigned(c); + } + } + out.setf(oldfmt, std::ios_base::basefield); + out.fill(oldfill); + return out; + } + +private: + std::string mData; +}; + +} // namespace LL + +#endif /* ! defined(LL_HEXDUMP_H) */ 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 3db03aec7d..c1dce97632 100644 --- a/indra/llcommon/llapp.cpp +++ b/indra/llcommon/llapp.cpp @@ -88,10 +88,6 @@ LLApp* LLApp::sApplication = NULL; // and disables crashlogger bool LLApp::sDisableCrashlogger = false; -// Local flag for whether or not to do logging in signal handlers. -//static -bool LLApp::sLogInSignal = true; - // static // Keeps track of application status LLScalarCond<LLApp::EAppStatus> LLApp::sStatus{LLApp::APP_STATUS_STOPPED}; @@ -314,7 +310,7 @@ void LLApp::stepFrame() { LLFrameTimer::updateFrameTime(); LLFrameTimer::updateFrameCount(); - LLEventTimer::updateClass(); + LLCallbackList::instance().callFunctions(); mRunner.run(); } @@ -596,6 +592,10 @@ void default_unix_signal_handler(int signum, siginfo_t *info, void *) // We do the somewhat sketchy operation of blocking in here until the error handler // has gracefully stopped the app. + // FIXME(brad) - we are using this handler for asynchronous signals as well, so sLogInSignal is currently + // disabled for safety. we need to find a way to selectively reenable it when it is safe. + // see issue secondlife/viewer#2566 + if (LLApp::sLogInSignal) { LL_INFOS() << "Signal handler - Got signal " << signum << " - " << apr_signal_description_get(signum) << LL_ENDL; diff --git a/indra/llcommon/llapp.h b/indra/llcommon/llapp.h index d90ecdf661..3d18864b80 100644 --- a/indra/llcommon/llapp.h +++ b/indra/llcommon/llapp.h @@ -339,8 +339,12 @@ private: friend void default_unix_signal_handler(int signum, siginfo_t *info, void *); #endif -public: - static bool sLogInSignal; +private: +#ifdef LL_RELEASE_FOR_DOWNLOAD + static constexpr bool sLogInSignal = false; +#else + static constexpr bool sLogInSignal = true; +#endif }; #endif // LL_LLAPP_H diff --git a/indra/llcommon/llapr.h b/indra/llcommon/llapr.h index 693cd7c01f..ff5c8c6a33 100644 --- a/indra/llcommon/llapr.h +++ b/indra/llcommon/llapr.h @@ -33,6 +33,7 @@ #include <sys/param.h> // Need PATH_MAX in APR headers... #endif +#include <memory> #include <boost/noncopyable.hpp> #include "llwin32headers.h" #include "apr_thread_proc.h" 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..1ae5c87a00 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; @@ -140,11 +137,20 @@ LLCoros::LLCoros(): // Previously we used // boost::context::guarded_stack_allocator::default_stacksize(); // empirically this is insufficient. - mStackSize(1024*1024), + mStackSize(512*1024), // mCurrent does NOT own the current CoroData instance -- it simply // 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() @@ -172,7 +178,7 @@ void LLCoros::cleanupSingleton() // don't use llcoro::suspend() because that module depends // on this one // This will yield current(main) thread and will let active - // corutines run once + // coroutines run once boost::this_fiber::yield(); } printActiveCoroutines("after pumping"); @@ -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 @@ -303,55 +309,6 @@ std::string LLCoros::launch(const std::string& prefix, const callable_t& callabl return name; } -namespace -{ - -#if LL_WINDOWS - -static const U32 STATUS_MSC_EXCEPTION = 0xE06D7363; // compiler specific - -U32 exception_filter(U32 code, struct _EXCEPTION_POINTERS *exception_infop) -{ - if (code == STATUS_MSC_EXCEPTION) - { - // C++ exception, go on - return EXCEPTION_CONTINUE_SEARCH; - } - else - { - // handle it - return EXCEPTION_EXECUTE_HANDLER; - } -} - -void sehandle(const LLCoros::callable_t& callable) -{ - __try - { - callable(); - } - __except (exception_filter(GetExceptionCode(), GetExceptionInformation())) - { - // convert to C++ styled exception - // Note: it might be better to use _se_set_translator - // if you want exception to inherit full callstack - char integer_string[512]; - sprintf(integer_string, "SEH, code: %lu\n", GetExceptionCode()); - throw std::exception(integer_string); - } -} - -#else // ! LL_WINDOWS - -inline void sehandle(const LLCoros::callable_t& callable) -{ - callable(); -} - -#endif // ! LL_WINDOWS - -} // anonymous namespace - // Top-level wrapper around caller's coroutine callable. // Normally we like to pass strings and such by const reference -- but in this // case, we WANT to copy both the name and the callable to our local stack! @@ -362,10 +319,11 @@ 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::seh::catcher(callable); } catch (const Stop& exc) { @@ -377,7 +335,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 +348,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 +373,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 +459,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/llerror.h b/indra/llcommon/llerror.h index 8a143ff30a..b17b9ff21e 100644 --- a/indra/llcommon/llerror.h +++ b/indra/llcommon/llerror.h @@ -95,6 +95,11 @@ const int LL_ERR_NOERR = 0; #define LL_STATIC_ASSERT(func, msg) static_assert(func, msg) #define LL_BAD_TEMPLATE_INSTANTIATION(type, msg) static_assert(false, msg) #else +#if LL_LINUX +// We need access to raise and SIGSEGV +#include <signal.h> +#endif + #define LL_STATIC_ASSERT(func, msg) BOOST_STATIC_ASSERT(func) #define LL_BAD_TEMPLATE_INSTANTIATION(type, msg) BOOST_STATIC_ASSERT(sizeof(type) != 0 && false); #endif @@ -408,12 +413,18 @@ typedef LLError::NoClassInfo _LL_CLASS_TO_LOG; #define LL_NEWLINE '\n' // Use this only in LL_ERRS or in a place that LL_ERRS may not be used + +#ifndef LL_LINUX #define LLERROR_CRASH \ { \ int* make_me_crash = (int*)0xDEADBEEFDEADBEEFUL; \ *make_me_crash = 0; \ exit(*make_me_crash); \ } +#else +// For Linux we just call raise and be done with it. No fighting the compiler to create a crashing code snippet. +#define LLERROR_CRASH raise(SIGSEGV ); +#endif #define LL_ENDL \ LLError::End(); \ 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.cpp b/indra/llcommon/llexception.cpp index c0154a569f..0a2f2ab607 100644 --- a/indra/llcommon/llexception.cpp +++ b/indra/llcommon/llexception.cpp @@ -15,6 +15,8 @@ #include "llexception.h" // STL headers // std headers +#include <iomanip> +#include <sstream> #include <typeinfo> // external library headers #include <boost/exception/diagnostic_information.hpp> @@ -29,7 +31,6 @@ // On Windows, header-only implementation causes macro collisions -- use // prebuilt library #define BOOST_STACKTRACE_LINK -#include <excpt.h> #endif // LL_WINDOWS #include <boost/stacktrace.hpp> @@ -94,25 +95,47 @@ void annotate_exception_(boost::exception& exc) // For windows SEH exception handling we sometimes need a filter that will // separate C++ exceptions from C SEH exceptions -static const U32 STATUS_MSC_EXCEPTION = 0xE06D7363; // compiler specific +static constexpr U32 STATUS_MSC_EXCEPTION = 0xE06D7363; // compiler specific +static constexpr U32 STATUS_STACK_FULL = 0xC00000FD; -U32 msc_exception_filter(U32 code, struct _EXCEPTION_POINTERS *exception_infop) +void LL::seh::fill_stacktrace(std::string& stacktrace, U32 code) { - const auto stack = to_string(boost::stacktrace::stacktrace()); - LL_WARNS() << "SEH Exception handled (that probably shouldn't be): Code " << code - << "\n Stack trace: \n" - << stack << LL_ENDL; + // Sadly, despite its diagnostic importance, trying to capture a + // stacktrace when the stack is already blown only terminates us faster. + if (code == STATUS_STACK_FULL) + { + stacktrace = "(stack overflow, no traceback)"; + } + else + { + stacktrace = to_string(boost::stacktrace::stacktrace()); + } +} +U32 LL::seh::common_filter(U32 code, struct _EXCEPTION_POINTERS*) +{ if (code == STATUS_MSC_EXCEPTION) { - // C++ exception, go on + // C++ exception, don't stop at this handler return EXCEPTION_CONTINUE_SEARCH; } else { - // handle it + // This is a non-C++ exception, e.g. hardware check. + // Pass control into the handler block. return EXCEPTION_EXECUTE_HANDLER; } } +void LL::seh::rethrow(U32 code, const std::string& stacktrace) +{ + std::ostringstream out; + out << "Windows exception 0x" << std::hex << code; + if (! stacktrace.empty()) + { + out << '\n' << stacktrace; + } + LLTHROW(Windows_SEH_exception(out.str())); +} + #endif //LL_WINDOWS diff --git a/indra/llcommon/llexception.h b/indra/llcommon/llexception.h index 68e609444e..0c32a55409 100644 --- a/indra/llcommon/llexception.h +++ b/indra/llcommon/llexception.h @@ -12,10 +12,16 @@ #if ! defined(LL_LLEXCEPTION_H) #define LL_LLEXCEPTION_H +#include "always_return.h" +#include "stdtypes.h" #include <stdexcept> +#include <utility> // std::forward #include <boost/exception/exception.hpp> #include <boost/throw_exception.hpp> #include <boost/current_function.hpp> +#if LL_WINDOWS +#include <excpt.h> +#endif // LL_WINDOWS // "Found someone who can comfort me // But there are always exceptions..." @@ -102,14 +108,115 @@ void crash_on_unhandled_exception_(const char*, int, const char*, const std::str log_unhandled_exception_(__FILE__, __LINE__, BOOST_CURRENT_FUNCTION, CONTEXT) void log_unhandled_exception_(const char*, int, const char*, const std::string&); +/***************************************************************************** +* Structured Exception Handling +*****************************************************************************/ +// this is used in platform-generic code -- define outside #if LL_WINDOWS +struct Windows_SEH_exception: public LLException +{ + Windows_SEH_exception(const std::string& what): LLException(what) {} +}; -#if LL_WINDOWS +namespace LL +{ +namespace seh +{ + +#if LL_WINDOWS //------------------------------------------------------------- + +void fill_stacktrace(std::string& stacktrace, U32 code); + +// wrapper around caller's U32 filter(U32 code, struct _EXCEPTION_POINTERS*) +// filter function: capture a stacktrace, if possible, before forwarding the +// call to the caller's filter() function +template <typename FILTER> +U32 filter_(std::string& stacktrace, FILTER&& filter, + U32 code, struct _EXCEPTION_POINTERS* exptrs) +{ + // By the time the handler gets control, the stack has been unwound, + // so report the stack trace now at filter() time. + fill_stacktrace(stacktrace, code); + return std::forward<FILTER>(filter)(code, exptrs); +} + +template <typename TRYCODE, typename FILTER, typename HANDLER> +auto catcher_inner(std::string& stacktrace, + TRYCODE&& trycode, FILTER&& filter, HANDLER&& handler) +{ + __try + { + return std::forward<TRYCODE>(trycode)(); + } + __except (filter_(stacktrace, + std::forward<FILTER>(filter), + GetExceptionCode(), GetExceptionInformation())) + { + return always_return<decltype(trycode())>( + std::forward<HANDLER>(handler), GetExceptionCode(), stacktrace); + } +} + +// triadic variant specifies try(), filter(U32, struct _EXCEPTION_POINTERS*), +// handler(U32, const std::string& stacktrace) +// stacktrace may or may not be available +template <typename TRYCODE, typename FILTER, typename HANDLER> +auto catcher(TRYCODE&& trycode, FILTER&& filter, HANDLER&& handler) +{ + // Construct and destroy this stacktrace string in the outer function + // because we can't do either in the function with __try/__except. + std::string stacktrace; + return catcher_inner(stacktrace, + std::forward<TRYCODE>(trycode), + std::forward<FILTER>(filter), + std::forward<HANDLER>(handler)); +} + +// common_filter() handles the typical case in which we want our handler +// clause to handle only Structured Exceptions rather than explicitly-thrown +// C++ exceptions +U32 common_filter(U32 code, struct _EXCEPTION_POINTERS*); + +// dyadic variant specifies try(), handler(U32, stacktrace), assumes common_filter() +template <typename TRYCODE, typename HANDLER> +auto catcher(TRYCODE&& trycode, HANDLER&& handler) +{ + return catcher(std::forward<TRYCODE>(trycode), + common_filter, + std::forward<HANDLER>(handler)); +} + +[[noreturn]] void rethrow(U32 code, const std::string& stacktrace); + +// monadic variant specifies try(), assumes default filter and handler +template <typename TRYCODE> +auto catcher(TRYCODE&& trycode) +{ + return catcher(std::forward<TRYCODE>(trycode), rethrow); +} + +#else // not LL_WINDOWS ----------------------------------------------------- + +template <typename TRYCODE, typename FILTER, typename HANDLER> +auto catcher(TRYCODE&& trycode, FILTER&&, HANDLER&&) +{ + return std::forward<TRYCODE>(trycode)(); +} + +template <typename TRYCODE, typename HANDLER> +auto catcher(TRYCODE&& trycode, HANDLER&&) +{ + return std::forward<TRYCODE>(trycode)(); +} + +template <typename TRYCODE> +auto catcher(TRYCODE&& trycode) +{ + return std::forward<TRYCODE>(trycode)(); +} -// SEH exception filtering for use in __try __except -// Separates C++ exceptions from C SEH exceptions -// Todo: might be good idea to do some kind of seh_to_msc_wrapper(function, ARGS&&); -U32 msc_exception_filter(U32 code, struct _EXCEPTION_POINTERS *exception_infop); +#endif // not LL_WINDOWS ----------------------------------------------------- -#endif //LL_WINDOWS +} // namespace LL::seh +} // namespace LL #endif /* ! defined(LL_LLEXCEPTION_H) */ 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..ef43af6ceb 100644 --- a/indra/llcommon/llleap.cpp +++ b/indra/llcommon/llleap.cpp @@ -14,23 +14,25 @@ // associated header #include "llleap.h" // STL headers -#include <sstream> #include <algorithm> +#include <memory> +#include <sstream> // std headers // external library headers // other Linden headers #include "llerror.h" -#include "llstring.h" -#include "llprocess.h" +#include "llerrorcontrol.h" #include "llevents.h" -#include "stringize.h" -#include "llsdutil.h" +#include "llexception.h" +#include "llleaplistener.h" +#include "llprocess.h" #include "llsdserialize.h" -#include "llerrorcontrol.h" +#include "llsdutil.h" +#include "llstring.h" #include "lltimer.h" #include "lluuid.h" -#include "llleaplistener.h" -#include "llexception.h" +#include "scriptcommand.h" +#include "stringize.h" #if LL_MSVC #pragma warning (disable : 4355) // 'this' used in initializer list: yes, intentionally @@ -50,20 +52,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()) @@ -107,7 +108,7 @@ public: // If that didn't work, no point in keeping this LLLeap object. if (! mChild) { - LLTHROW(Error(STRINGIZE("failed to run " << mDesc))); + LLTHROW(Error(stringize("failed to run ", mDesc))); } // Okay, launch apparently worked. Change our mDonePump listener. @@ -122,9 +123,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 +144,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 +191,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 +342,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 +353,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 +421,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 +438,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; @@ -501,12 +485,24 @@ LLLeap* LLLeap::create(const std::string& desc, const std::vector<std::string>& LLLeap* LLLeap::create(const std::string& desc, const std::string& plugin, bool exc) { - // Use LLStringUtil::getTokens() to parse the command line - return create(desc, - LLStringUtil::getTokens(plugin, - " \t\r\n", // drop_delims - "", // no keep_delims - "\"'", // either kind of quotes - "\\"), // backslash escape - exc); + // Use ScriptCommand to parse the command line + ScriptCommand command(plugin); + auto error = command.error(); + if (! error.empty()) + { + if (exc) + { + LLTHROW(Error(error)); + } + return nullptr; + } + + LLProcess::Params params; + params.desc = desc; + params.executable = command.script; + for (const auto& arg : command.args) + { + params.args.add(arg); + } + return create(params, exc); } 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/llmutex.h b/indra/llcommon/llmutex.h index 6e8cf9643b..62943845a5 100644 --- a/indra/llcommon/llmutex.h +++ b/indra/llcommon/llmutex.h @@ -194,6 +194,18 @@ public: mSharedMutex->unlock<SHARED>(); } + void lock() + { + if (mSharedMutex) + mSharedMutex->lock<SHARED>(); + } + + void unlock() + { + if (mSharedMutex) + mSharedMutex->unlock<SHARED>(); + } + private: LLSharedMutex* mSharedMutex; }; diff --git a/indra/llcommon/llprocessor.cpp b/indra/llcommon/llprocessor.cpp index 41ff369de2..51b1b967f1 100644 --- a/indra/llcommon/llprocessor.cpp +++ b/indra/llcommon/llprocessor.cpp @@ -793,19 +793,55 @@ private: }; #elif LL_LINUX + +// *NOTE:Mani - eww, macros! srry. +#define LLPI_SET_INFO_STRING(llpi_id, cpuinfo_id) \ + if (!cpuinfo[cpuinfo_id].empty()) \ + { setInfo(llpi_id, cpuinfo[cpuinfo_id]);} + +#define LLPI_SET_INFO_INT(llpi_id, cpuinfo_id) \ + {\ + S32 result; \ + if (!cpuinfo[cpuinfo_id].empty() \ + && LLStringUtil::convertToS32(cpuinfo[cpuinfo_id], result)) \ + { setInfo(llpi_id, result);} \ + } + const char CPUINFO_FILE[] = "/proc/cpuinfo"; -class LLProcessorInfoLinuxImpl : public LLProcessorInfoImpl -{ +class LLProcessorInfoLinuxImpl : public LLProcessorInfoImpl { public: - LLProcessorInfoLinuxImpl() - { + LLProcessorInfoLinuxImpl() { get_proc_cpuinfo(); } virtual ~LLProcessorInfoLinuxImpl() {} + private: + F64 getCPUMaxMHZ() + { + // Nicky: We just look into cpu0. In theory we could iterate over all cores + // "/sys/devices/system/cpu/cpu*/cpufreq/cpuinfo_max_freq" + // But those should not fluctuate that much? + std::ifstream fIn { "/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq" }; + + if( !fIn.is_open() ) + return 0.0; + + std::string strLine; + fIn >> strLine; + if( strLine.empty() ) + return 0.0l; + + F64 mhz {}; + if( !LLStringUtil::convertToF64(strLine, mhz ) ) + return 0.0; + + mhz = mhz / 1000.0; + return mhz; + } + void get_proc_cpuinfo() { std::map< std::string, std::string > cpuinfo; @@ -834,30 +870,23 @@ private: std::string llinename(linename); LLStringUtil::toLower(llinename); std::string lineval( spacespot + 1, nlspot ); - cpuinfo[ llinename ] = lineval; + cpuinfo[ llinename ] = lineval; } fclose(cpuinfo_fp); } # if LL_X86 -// *NOTE:Mani - eww, macros! srry. -#define LLPI_SET_INFO_STRING(llpi_id, cpuinfo_id) \ - if (!cpuinfo[cpuinfo_id].empty()) \ - { setInfo(llpi_id, cpuinfo[cpuinfo_id]);} - -#define LLPI_SET_INFO_INT(llpi_id, cpuinfo_id) \ - {\ - S32 result; \ - if (!cpuinfo[cpuinfo_id].empty() \ - && LLStringUtil::convertToS32(cpuinfo[cpuinfo_id], result)) \ - { setInfo(llpi_id, result);} \ + F64 mhzFromSys = getCPUMaxMHZ(); + F64 mhzFromProc {}; + if( !LLStringUtil::convertToF64(cpuinfo["cpu mhz"], mhzFromProc ) ) + mhzFromProc = 0.0; + if (mhzFromSys > 1.0 && mhzFromSys > mhzFromProc ) + { + setInfo( eFrequency, mhzFromSys ); } - - F64 mhz; - if (LLStringUtil::convertToF64(cpuinfo["cpu mhz"], mhz) - && 200.0 < mhz && mhz < 10000.0) + else if ( 200.0 < mhzFromProc && mhzFromProc < 10000.0) { - setInfo(eFrequency,(F64)(mhz)); + setInfo(eFrequency,(F64)(mhzFromProc)); } LLPI_SET_INFO_STRING(eBrandName, "model name"); @@ -867,7 +896,7 @@ private: LLPI_SET_INFO_INT(eModel, "model"); - S32 family; + S32 family{}; if (!cpuinfo["cpu family"].empty() && LLStringUtil::convertToS32(cpuinfo["cpu family"], family)) { diff --git a/indra/llcommon/llrand.cpp b/indra/llcommon/llrand.cpp index 2c51e6f07f..513613f543 100644 --- a/indra/llcommon/llrand.cpp +++ b/indra/llcommon/llrand.cpp @@ -58,9 +58,26 @@ * to restore uniform distribution. */ -// gRandomGenerator is a stateful static object, which is therefore not +// pRandomGenerator is a stateful static object, which is therefore not // inherently thread-safe. -static thread_local LLRandLagFib2281 gRandomGenerator(LLUUID::getRandomSeed()); +//We use a pointer to not construct a huge object in the TLS space, sadly this is necessary +// due to libcef.so on Linux being compiled with TLS model initial-exec (resulting in +// FLAG STATIC_TLS, see readelf libcef.so). CEFs own TLS objects + LLRandLagFib2281 then will exhaust the +// available TLS space, causing media failure. + +static thread_local std::unique_ptr< LLRandLagFib2281 > pRandomGenerator = nullptr; + +namespace { + F64 ll_internal_get_rand() + { + if( !pRandomGenerator ) + { + pRandomGenerator.reset(new LLRandLagFib2281(LLUUID::getRandomSeed( ) )); + } + + return(*pRandomGenerator)(); + } +} // no default implementation, only specific F64 and F32 specializations template <typename REAL> @@ -73,7 +90,7 @@ inline F64 ll_internal_random<F64>() // CPUs (or at least multi-threaded processes) seem to // occasionally give an obviously incorrect random number -- like // 5^15 or something. Sooooo, clamp it as described above. - F64 rv{ gRandomGenerator() }; + F64 rv{ ll_internal_get_rand() }; if(!((rv >= 0.0) && (rv < 1.0))) return fmod(rv, 1.0); return rv; } @@ -85,7 +102,7 @@ inline F32 ll_internal_random<F32>() // Per Monty, it's important to clamp using the correct fmodf() rather // than expanding to F64 for fmod() and then truncating back to F32. Prior // to this change, we were getting sporadic ll_frand() == 1.0 results. - F32 rv{ narrow<F64>(gRandomGenerator()) }; + F32 rv{ narrow<F64>(ll_internal_get_rand()) }; if(!((rv >= 0.0f) && (rv < 1.0f))) return fmodf(rv, 1.0f); return rv; } 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/llsdjson.cpp b/indra/llcommon/llsdjson.cpp index 5d38e55686..1df2a8f9eb 100644 --- a/indra/llcommon/llsdjson.cpp +++ b/indra/llcommon/llsdjson.cpp @@ -61,12 +61,20 @@ LLSD LlsdFromJson(const boost::json::value& val) result = LLSD(val.as_bool()); break; case boost::json::kind::array: + { result = LLSD::emptyArray(); - for (const auto &element : val.as_array()) + auto& array = val.as_array(); + // allocate elements 0 .. (size() - 1) to avoid incremental allocation + if (! array.empty()) + { + result[array.size() - 1] = LLSD(); + } + for (const auto &element : array) { result.append(LlsdFromJson(element)); } break; + } case boost::json::kind::object: result = LLSD::emptyMap(); for (const auto& element : val.as_object()) @@ -106,6 +114,7 @@ boost::json::value LlsdToJson(const LLSD &val) case LLSD::TypeMap: { boost::json::object& obj = result.emplace_object(); + obj.reserve(val.size()); for (const auto& llsd_dat : llsd::inMap(val)) { obj[llsd_dat.first] = LlsdToJson(llsd_dat.second); @@ -115,6 +124,7 @@ boost::json::value LlsdToJson(const LLSD &val) case LLSD::TypeArray: { boost::json::array& json_array = result.emplace_array(); + json_array.reserve(val.size()); for (const auto& llsd_dat : llsd::inArray(val)) { json_array.push_back(LlsdToJson(llsd_dat)); @@ -123,7 +133,8 @@ boost::json::value LlsdToJson(const LLSD &val) } case LLSD::TypeBinary: default: - LL_ERRS("LlsdToJson") << "Unsupported conversion to JSON from LLSD type (" << val.type() << ")." << LL_ENDL; + LL_ERRS("LlsdToJson") << "Unsupported conversion to JSON from LLSD type (" + << val.type() << ")." << LL_ENDL; break; } diff --git a/indra/llcommon/llsdutil.cpp b/indra/llcommon/llsdutil.cpp index dbd89118c9..fda47c7a87 100644 --- a/indra/llcommon/llsdutil.cpp +++ b/indra/llcommon/llsdutil.cpp @@ -160,7 +160,7 @@ LLSD ll_binary_from_string(const LLSD& sd) char* ll_print_sd(const LLSD& sd) { const U32 bufferSize = 10 * 1024; - static char buffer[bufferSize]; + static char buffer[bufferSize + 1]; std::ostringstream stream; //stream.rdbuf()->pubsetbuf(buffer, bufferSize); stream << LLSDOStreamer<LLSDXMLFormatter>(sd); @@ -182,7 +182,7 @@ char* ll_pretty_print_sd_ptr(const LLSD* sd) char* ll_pretty_print_sd(const LLSD& sd) { const U32 bufferSize = 100 * 1024; - static char buffer[bufferSize]; + static char buffer[bufferSize + 1]; std::ostringstream stream; //stream.rdbuf()->pubsetbuf(buffer, bufferSize); stream << LLSDOStreamer<LLSDXMLFormatter>(sd, LLSDFormatter::OPTIONS_PRETTY); 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/llsys.cpp b/indra/llcommon/llsys.cpp index 3f33ad61c5..7e69ddf6fc 100644 --- a/indra/llcommon/llsys.cpp +++ b/indra/llcommon/llsys.cpp @@ -504,57 +504,46 @@ const S32 LLOSInfo::getOSBitness() const return mOSBitness; } -//static -U32 LLOSInfo::getProcessVirtualSizeKB() -{ - U32 virtual_size = 0; -#if LL_LINUX -# define STATUS_SIZE 2048 - LLFILE* status_filep = LLFile::fopen("/proc/self/status", "rb"); - if (status_filep) - { - S32 numRead = 0; - char buff[STATUS_SIZE]; /* Flawfinder: ignore */ +namespace { - size_t nbytes = fread(buff, 1, STATUS_SIZE-1, status_filep); - buff[nbytes] = '\0'; + U32 readFromProcStat( std::string entryName ) + { + U32 val{}; +#if LL_LINUX + constexpr U32 STATUS_SIZE = 2048; - // All these guys return numbers in KB - char *memp = strstr(buff, "VmSize:"); - if (memp) + LLFILE* status_filep = LLFile::fopen("/proc/self/status", "rb"); + if (status_filep) { - numRead += sscanf(memp, "%*s %u", &virtual_size); + char buff[STATUS_SIZE]; /* Flawfinder: ignore */ + + size_t nbytes = fread(buff, 1, STATUS_SIZE-1, status_filep); + buff[nbytes] = '\0'; + + // All these guys return numbers in KB + char *memp = strstr(buff, entryName.c_str()); + if (memp) + { + (void) sscanf(memp, "%*s %u", &val); + } + fclose(status_filep); } - fclose(status_filep); - } #endif - return virtual_size; + return val; + } + } //static -U32 LLOSInfo::getProcessResidentSizeKB() +U32 LLOSInfo::getProcessVirtualSizeKB() { - U32 resident_size = 0; -#if LL_LINUX - LLFILE* status_filep = LLFile::fopen("/proc/self/status", "rb"); - if (status_filep != NULL) - { - S32 numRead = 0; - char buff[STATUS_SIZE]; /* Flawfinder: ignore */ - - size_t nbytes = fread(buff, 1, STATUS_SIZE-1, status_filep); - buff[nbytes] = '\0'; + return readFromProcStat( "VmSize:" ); +} - // All these guys return numbers in KB - char *memp = strstr(buff, "VmRSS:"); - if (memp) - { - numRead += sscanf(memp, "%*s %u", &resident_size); - } - fclose(status_filep); - } -#endif - return resident_size; +//static +U32 LLOSInfo::getProcessResidentSizeKB() +{ + return readFromProcStat( "VmRSS:" ); } //static @@ -1118,6 +1107,14 @@ LLSD LLMemoryInfo::loadStatsMap() LLSD::String key(matched[1].first, matched[1].second); LLSD::String value_str(matched[2].first, matched[2].second); LLSD::Integer value(0); + + // Skip over VmallocTotal. It's just a fixed and huge number on (modern) systems. "34359738367 kB" + // https://unix.stackexchange.com/questions/700724/why-is-vmalloctotal-34359738367-kb + // If not skipped converting it to a LLSD::integer (32 bit) will fail and spam the logs (this function + // is called quite frequently). + if( key == "VmallocTotal") + continue; + try { value = boost::lexical_cast<LLSD::Integer>(value_str); diff --git a/indra/llcommon/llthread.h b/indra/llcommon/llthread.h index 4194e0014d..21264351e5 100644 --- a/indra/llcommon/llthread.h +++ b/indra/llcommon/llthread.h @@ -68,7 +68,7 @@ public: // Called from MAIN THREAD. void pause(); void unpause(); - bool isPaused() { return isStopped() || mPaused; } + bool isPaused() const { return isStopped() || mPaused; } // Cause the thread to wake up and check its condition void wake(); 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..a9f88f3170 --- /dev/null +++ b/indra/llcommon/lua_function.cpp @@ -0,0 +1,1722 @@ +/** + * @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, + const std::vector<std::string>& args) +{ + // debug.traceback() + compiled chunk + args table + args... + slop + lluau_checkstack(L, 1 + 1 + 1 + int(args.size()) + 2); + 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 + // capture absolute index of debug.traceback() + int traceback = lua_absindex(L, -2); + // remove it from stack on exit + LuaRemover cleanup(L, traceback); + + // Originally we just pushed 'args' to the Lua stack before entering the + // chunk. But that's awkward for the chunk: it must reference those + // arguments as '...', using any of a number of tactics to move them to + // named variables. This doesn't work as a Lua chunk expecting arguments: + // function(a, b, c) + // -- ... + // end + // because that code only *defines* a function: the function's body isn't + // entered by executing the chunk. + // + // Per https://www.lua.org/manual/5.1/manual.html#6 Lua Stand-alone, we + // now also create a global table called 'arg' whose [0] is the script + // name, ['n'] is the number of additional arguments and [1] through + // [['n']] are the additional arguments. We diverge from that spec in not + // creating any negative indices. + // + // Since the spec notes that the chunk can also reference args using + // '...', we also leave them on the stack. + + // stack: debug.traceback(), compiled chunk + // create arg table pre-sized to hold the args array, plus [0] and ['n'] + lua_createtable(L, narrow(args.size()), 2); + // stack: debug.traceback(), compiled chunk, arg table + int argi = lua_absindex(L, -1); + lua_Integer i = 0; + // store desc (e.g. script name) as arg[0] + lua_pushinteger(L, i); + lua_pushstdstring(L, desc); + lua_rawset(L, argi); // rawset() pops key and value + // store args.size() as arg.n + lua_pushinteger(L, narrow(args.size())); + lua_setfield(L, argi, "n"); // setfield() pops value + for (const auto& arg : args) + { + // push each arg in order + lua_pushstdstring(L, arg); + // push index + lua_pushinteger(L, ++i); + // duplicate arg[i] to store in arg table + lua_pushvalue(L, -2); + // stack: ..., arg[i], i, arg[i] + lua_rawset(L, argi); + // leave ..., arg[i] on stack + } + // stack: debug.traceback(), compiled chunk, arg, arg[1], arg[2], ... + // duplicate the arg table to store it + lua_pushvalue(L, argi); + lua_setglobal(L, "arg"); + lua_remove(L, argi); + // stack: debug.traceback(), compiled chunk, arg[1], arg[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, int(args.size()), LUA_MULTRET, traceback); +} + +int loadstring(lua_State *L, const std::string &desc, const std::string &text) +{ + lluau_checkstack(L, 1); + 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() +{ + /*---------------------------- 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) + return; + /*---------------------------- 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); +} + +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, + const std::vector<std::string>& args) +{ + /*---------------------------- 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, args))) + { + 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.suspend") << 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..ae6e0bf7ba --- /dev/null +++ b/indra/llcommon/lua_function.h @@ -0,0 +1,626 @@ +/** + * @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 "scriptcommand.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 +#include <vector> + +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. Any args are pushed to the Lua stack before + // calling the Lua chunk in text. + int dostring(lua_State* L, const std::string& desc, const std::string& text, + const std::vector<std::string>& args={}); + 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: + LuaState(); + + 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. + // Pass: + // desc = description used for logging et al. + // text = Lua chunk to execute, e.g. contents of a script file + // args = arguments, if any, to pass to script file + // Returns: + // 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, + const std::vector<std::string>& args={}); + + 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 ----------------------------*/ + 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/scriptcommand.cpp b/indra/llcommon/scriptcommand.cpp new file mode 100644 index 0000000000..79afbc2063 --- /dev/null +++ b/indra/llcommon/scriptcommand.cpp @@ -0,0 +1,117 @@ +/** + * @file scriptcommand.cpp + * @author Nat Goodspeed + * @date 2024-09-16 + * @brief Implementation for scriptcommand. + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "scriptcommand.h" +// STL headers +// std headers +#include <sstream> +// external library headers +// other Linden headers +#include "fsyspath.h" +#include "llerror.h" +#include "llsdutil.h" +#include "llstring.h" +#include "stringize.h" + +ScriptCommand::ScriptCommand(const std::string& command, const LLSD& path, + const std::string& base) +{ + fsyspath basepath(base); + // Use LLStringUtil::getTokens() to parse the script command line + args = LLStringUtil::getTokens(command, + " \t\r\n", // drop_delims + "", // no keep_delims + "\"'", // either kind of quotes + "\\"); // backslash escape + // search for args[0] on paths + if (search(args[0], path, basepath)) + { + // The first token is in fact the script filename. Now that we've + // found the script file, we've consumed that token. The rest are + // command-line arguments. + args.erase(args.begin()); + return; + } + + // Parsing the command line produced a script file path we can't find. + // Maybe that's because there are spaces in the original pathname that + // were neither quoted nor escaped? See if we can find the whole original + // command line string. + if (search(command, path, basepath)) + { + // Here we found script, using the whole input command line as its + // pathname. Discard any parts of it we mistook for command-line + // arguments. + args.clear(); + return; + } + + // Couldn't find the script either way. Is it because we can't even check + // existence? + if (! mError.empty()) + { + return; + } + + // No, existence check works, we just can't find the script. + std::ostringstream msgstream; + msgstream << "Can't find script file " << std::quoted(args[0]); + if (command != args[0]) + { + msgstream << " or " << std::quoted(command); + } + if (path.size() > 0) + { + msgstream << " on " << path; + } + if (! base.empty()) + { + msgstream << " relative to " << base; + } + mError = msgstream.str(); + LL_WARNS("Lua") << mError << LL_ENDL; +} + +bool ScriptCommand::search(const fsyspath& script, const LLSD& paths, const fsyspath& base) +{ + for (const auto& path : llsd::inArray(paths)) + { + // If a path is already absolute, (otherpath / path) preserves it. + // Explicitly instantiate fsyspath for every string conversion to + // properly convert UTF-8 filename strings on Windows. + fsyspath absscript{ base / fsyspath(path.asString()) / script }; + bool exists; + try + { + exists = std::filesystem::exists(absscript); + } + catch (const std::filesystem::filesystem_error& exc) + { + mError = stringize("Can't check existence: ", exc.what()); + LL_WARNS("Lua") << mError << LL_ENDL; + return false; + } + if (exists) + { + this->script = absscript.string(); + return true; + } + } + return false; +} + +std::string ScriptCommand::error() const +{ + return mError; +} diff --git a/indra/llcommon/scriptcommand.h b/indra/llcommon/scriptcommand.h new file mode 100644 index 0000000000..dafed552bb --- /dev/null +++ b/indra/llcommon/scriptcommand.h @@ -0,0 +1,64 @@ +/** + * @file scriptcommand.h + * @author Nat Goodspeed + * @date 2024-09-16 + * @brief ScriptCommand parses a string into a script filepath plus arguments. + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_SCRIPTCOMMAND_H) +#define LL_SCRIPTCOMMAND_H + +#include "llsd.h" + +#include <string> +#include <vector> + +class fsyspath; + +class ScriptCommand +{ +public: + /** + * ScriptCommand accepts a command-line string to invoke an existing + * script, which may or may not include arguments to pass to that script. + * The constructor parses the command line into tokens using quoting and + * escaping rules similar to bash. The first token is assumed to be the + * script name; ScriptCommand tries to find that script on the filesystem, + * trying each directory listed in the LLSD array of strings 'path'. When + * it finds the script file, it stores its full pathname in 'script' and + * the remaining tokens in 'args'. + * + * But if the constructor can't find the first token on 'path', it + * considers the possibility that the whole command-line string is + * actually a single pathname containing unescaped spaces. If that script + * file is found on 'path', it stores its full pathname in 'script' and + * leaves 'args' empty. + * + * We accept 'path' as an LLSD array rather than (e.g.) + * std::vector<std::string> because the primary use case involves + * retrieving 'path' from settings. + * + * If you also pass 'base', any directory on 'path' may be specified + * relative to 'base'. Otherwise, every directory on 'path' must be + * absolute. + */ + ScriptCommand(const std::string& command, const LLSD& path=LLSD::emptyArray(), + const std::string& base={}); + + std::string script; + std::vector<std::string> args; + + // returns empty string if no error + std::string error() const; + +private: + bool search(const fsyspath& script, const LLSD& path, const fsyspath& base); + + std::string mError; +}; + +#endif /* ! defined(LL_SCRIPTCOMMAND_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..773b373e72 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) @@ -35,7 +35,7 @@ // causes Windows abdominal pain such that it later fails code-signing in some // mysterious way. Entirely suppressing these LLLeap tests pushes the failure // rate MUCH lower. Can we re-enable them with a smaller data size on Windows? -const size_t BUFFERED_LENGTH = 100*1024; +const size_t BUFFERED_LENGTH = 1023*1024; #else // not Windows const size_t BUFFERED_LENGTH = 1023*1024; // try wrangling just under a megabyte of data @@ -300,8 +300,8 @@ namespace tut std::string threw = catch_what<LLLeap::Error>([&BADPYTHON](){ LLLeap::create("bad exe", BADPYTHON); }); - ensure_contains("LLLeap::create() didn't throw", threw, "failed"); - log.messageWith("failed"); + ensure_contains("LLLeap::create() didn't throw", threw, "Can't find"); + log.messageWith("Can't find"); log.messageWith(BADPYTHON); // try the suppress-exception variant ensure("bad launch returned non-NULL", ! LLLeap::create("bad exe", BADPYTHON, false)); @@ -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/tests/llstring_test.cpp b/indra/llcommon/tests/llstring_test.cpp index b18712b8e9..b68c63a15f 100644 --- a/indra/llcommon/tests/llstring_test.cpp +++ b/indra/llcommon/tests/llstring_test.cpp @@ -377,7 +377,7 @@ namespace tut { F32 value; std::string str_val("2147483647"); //0x7FFFFFFF - ensure("1: convertToF32 failed", LLStringUtil::convertToF32(str_val, value) && value == 2147483647); + ensure("1: convertToF32 failed", LLStringUtil::convertToF32(str_val, value) && value == 2147483647.f); str_val = "0"; ensure("2: convertToF32 failed", LLStringUtil::convertToF32(str_val, value) && value == 0); @@ -399,7 +399,7 @@ namespace tut { F64 value; std::string str_val("9223372036854775807"); //0x7FFFFFFFFFFFFFFF - ensure("1: convertToF64 failed", LLStringUtil::convertToF64(str_val, value) && value == 9223372036854775807LL); + ensure("1: convertToF64 failed", LLStringUtil::convertToF64(str_val, value) && value == 9223372036854775807.); str_val = "0"; ensure("2: convertToF64 failed", LLStringUtil::convertToF64(str_val, value) && value == 0.0F); 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)...); |