/** * @file lleventcoro.cpp * @author Nat Goodspeed * @date 2009-04-29 * @brief Implementation for lleventcoro. * * $LicenseInfo:firstyear=2009&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$ */ // Precompiled header #include "linden_common.h" // associated header #include "lleventcoro.h" // STL headers #include <chrono> #include <exception> // std headers // external library headers #include <boost/fiber/operations.hpp> // other Linden headers #include "llsdserialize.h" #include "llsdutil.h" #include "llerror.h" #include "llcoros.h" #include "stringize.h" namespace { /** * suspendUntilEventOn() permits a coroutine to temporarily listen on an * LLEventPump any number of times. We don't really want to have to ask * the caller to label each such call with a distinct string; the whole * point of suspendUntilEventOn() is to present a nice sequential interface to * the underlying LLEventPump-with-named-listeners machinery. So we'll use * LLEventPump::inventName() to generate a distinct name for each * temporary listener. On the other hand, because a given coroutine might * call suspendUntilEventOn() any number of times, we don't really want to * consume an arbitrary number of generated inventName()s: that namespace, * though large, is nonetheless finite. So we memoize an invented name for * each distinct coroutine instance. */ std::string listenerNameForCoro() { // If this coroutine was launched by LLCoros::launch(), find that name. std::string name(LLCoros::getName()); if (! name.empty()) { return name; } // this is the first time we've been called for this coroutine instance name = LLEventPump::inventName("coro"); LL_INFOS("LLEventCoro") << "listenerNameForCoro(): inventing coro name '" << name << "'" << LL_ENDL; return name; } /** * Implement behavior described for postAndSuspend()'s @a replyPumpNamePath * parameter: * * * If <tt>path.isUndefined()</tt>, do nothing. * * If <tt>path.isString()</tt>, @a dest is an LLSD map: store @a value * into <tt>dest[path.asString()]</tt>. * * If <tt>path.isInteger()</tt>, @a dest is an LLSD array: store @a * value into <tt>dest[path.asInteger()]</tt>. * * If <tt>path.isArray()</tt>, iteratively apply the rules above to step * down through the structure of @a dest. The last array entry in @a * path specifies the entry in the lowest-level structure in @a dest * into which to store @a value. * * @note * In the degenerate case in which @a path is an empty array, @a dest will * @em become @a value rather than @em containing it. */ void storeToLLSDPath(LLSD& dest, const LLSD& path, const LLSD& value) { if (path.isUndefined()) { // no-op case return; } // Drill down to where we should store 'value'. llsd::drill_ref(dest, path) = value; } } // anonymous void llcoro::suspend() { LLCoros::checkStop(); LLCoros::TempStatus st("waiting one tick"); boost::this_fiber::yield(); } void llcoro::suspendUntilTimeout(float seconds) { LLCoros::checkStop(); // 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 // 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 // the LLEventPumps LLSingleton registry at static initialization time. // DO allow tweaking the name for uniqueness, this definitely gets // re-entered on multiple coroutines! // We could use an LLUUID if it were important to actively prohibit anyone // from ever posting on this LLEventPump. LLEventStream bogus("xyzzy", true); // 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. suspendUntilEventOnWithTimeout(bogus, seconds, timedout); } namespace { // returns a listener on replyPumpP, also on "mainloop" -- both should be // stored in LLTempBoundListeners on the caller's stack frame std::pair<LLBoundListener, LLBoundListener> postAndSuspendSetup(const std::string& callerName, const std::string& listenerName, LLCoros::Promise<LLSD>& promise, const LLSD& event, const LLEventPumpOrPumpName& requestPumpP, const LLEventPumpOrPumpName& replyPumpP, const LLSD& replyPumpNamePath) { // Before we get any farther -- should we be stopping instead of // suspending? LLCoros::checkStop(); // Get the consuming attribute for THIS coroutine, the one that's about to // suspend. Don't call get_consuming() in the lambda body: that would // return the consuming attribute for some other coroutine, most likely // the main routine. bool consuming(LLCoros::get_consuming()); // listen on the specified LLEventPump with a lambda that will assign a // value to the promise, thus fulfilling its future llassert_always_msg(replyPumpP, ("replyPump required for " + callerName)); LLEventPump& replyPump(replyPumpP.getPump()); // The relative order of the two listen() calls below would only matter if // "LLApp" were an LLEventMailDrop. But if we ever go there, we'd want to // notice the pending LLApp status first. LLBoundListener stopper( LLCoros::getStopListener( listenerName, LLCoros::instance().getName(), [&promise, listenerName](const LLSD& status) { 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_WARNS("lleventcoro") << listenerName << " couldn't throw Stopping " "because promise already set" << LL_ENDL; } })); LLBoundListener connection( replyPump.listen( listenerName, [&promise, consuming, listenerName](const LLSD& result) { try { promise.set_value(result); // We did manage to propagate the result value to the // (real) listener. If we're supposed to indicate that // we've consumed it, do so. return consuming; } catch(boost::fibers::promise_already_satisfied & ex) { LL_DEBUGS("lleventcoro") << "promise already satisfied in '" << listenerName << "': " << ex.what() << LL_ENDL; // We could not propagate the result value to the // listener. return false; } })); // skip the "post" part if requestPump is default-constructed if (requestPumpP) { LLEventPump& requestPump(requestPumpP.getPump()); // If replyPumpNamePath is non-empty, store the replyPump name in the // request event. LLSD modevent(event); storeToLLSDPath(modevent, replyPumpNamePath, replyPump.getName()); LL_DEBUGS("lleventcoro") << callerName << ": coroutine " << listenerName << " posting to " << requestPump.getName() << LL_ENDL; // *NOTE:Mani - Removed because modevent could contain user's hashed passwd. // << ": " << modevent << LL_ENDL; requestPump.post(modevent); } LL_DEBUGS("lleventcoro") << callerName << ": coroutine " << listenerName << " about to wait on LLEventPump " << replyPump.getName() << LL_ENDL; return { connection, stopper }; } } // anonymous LLSD llcoro::postAndSuspend(const LLSD& event, const LLEventPumpOrPumpName& requestPump, const LLEventPumpOrPumpName& replyPump, const LLSD& replyPumpNamePath) { LLCoros::Promise<LLSD> promise; std::string listenerName(listenerNameForCoro()); // Store both connections into LLTempBoundListeners so we implicitly // disconnect on return from this function. auto connections = postAndSuspendSetup("postAndSuspend()", listenerName, promise, event, requestPump, replyPump, replyPumpNamePath); LLTempBoundListener connection(connections.first), stopper(connections.second); // declare the future LLCoros::Future<LLSD> future = LLCoros::getFuture(promise); // calling get() on the future makes us wait for it LLCoros::TempStatus st(STRINGIZE("waiting for " << replyPump.getPump().getName())); LLSD value(future.get()); LL_DEBUGS("lleventcoro") << "postAndSuspend(): coroutine " << listenerName << " resuming with " << value << LL_ENDL; // returning should disconnect the connection return value; } LLSD llcoro::postAndSuspendWithTimeout(const LLSD& event, const LLEventPumpOrPumpName& requestPump, const LLEventPumpOrPumpName& replyPump, const LLSD& replyPumpNamePath, F32 timeout, const LLSD& timeoutResult) { LLCoros::Promise<LLSD> promise; std::string listenerName(listenerNameForCoro()); // Store both connections into LLTempBoundListeners so we implicitly // disconnect on return from this function. auto connections = postAndSuspendSetup("postAndSuspendWithTimeout()", listenerName, promise, event, requestPump, replyPump, replyPumpNamePath); LLTempBoundListener connection(connections.first), stopper(connections.second); // declare the future LLCoros::Future<LLSD> future = LLCoros::getFuture(promise); // wait for specified timeout boost::fibers::future_status status; { LLCoros::TempStatus st(STRINGIZE("waiting for " << replyPump.getPump().getName() << " for " << timeout << "s")); // The fact that we accept non-integer seconds means we should probably // use granularity finer than one second. However, given the overhead of // the rest of our processing, it seems silly to use granularity finer // than a millisecond. status = future.wait_for(std::chrono::milliseconds(long(timeout * 1000))); } // if the future is NOT yet ready, return timeoutResult instead if (status == boost::fibers::future_status::timeout) { LL_DEBUGS("lleventcoro") << "postAndSuspendWithTimeout(): coroutine " << listenerName << " timed out after " << timeout << " seconds," << " resuming with " << timeoutResult << LL_ENDL; return timeoutResult; } else { llassert_always(status == boost::fibers::future_status::ready); // future is now ready, no more waiting LLSD value(future.get()); LL_DEBUGS("lleventcoro") << "postAndSuspendWithTimeout(): coroutine " << listenerName << " resuming with " << value << LL_ENDL; // returning should disconnect the connection return value; } }