summaryrefslogtreecommitdiff
path: root/indra/newview
diff options
context:
space:
mode:
Diffstat (limited to 'indra/newview')
-rw-r--r--indra/newview/llluamanager.cpp9
-rw-r--r--indra/newview/scripts/lua/ErrorQueue.lua30
-rw-r--r--indra/newview/scripts/lua/Queue.lua41
-rw-r--r--indra/newview/scripts/lua/WaitQueue.lua80
-rw-r--r--indra/newview/scripts/lua/inspect.lua371
-rw-r--r--indra/newview/scripts/lua/leap.lua393
-rw-r--r--indra/newview/scripts/lua/qtest.lua131
-rw-r--r--indra/newview/scripts/lua/util.lua72
-rw-r--r--indra/newview/tests/llluamanager_test.cpp34
9 files changed, 1129 insertions, 32 deletions
diff --git a/indra/newview/llluamanager.cpp b/indra/newview/llluamanager.cpp
index cd663021a1..c65f062fd7 100644
--- a/indra/newview/llluamanager.cpp
+++ b/indra/newview/llluamanager.cpp
@@ -435,7 +435,14 @@ void LLRequireResolver::findModule()
fail();
}
- std::vector<std::string> lib_paths {gDirUtilp->getExpandedFilename(LL_PATH_SCRIPTS, "lua")};
+ std::vector<std::string> lib_paths
+ {
+ gDirUtilp->getExpandedFilename(LL_PATH_SCRIPTS, "lua"),
+#ifdef LL_TEST
+ // Build-time tests don't have the app bundle - use source tree.
+ std::filesystem::path(__FILE__).parent_path() / "scripts" / "lua",
+#endif
+ };
for (const auto& path : lib_paths)
{
diff --git a/indra/newview/scripts/lua/ErrorQueue.lua b/indra/newview/scripts/lua/ErrorQueue.lua
new file mode 100644
index 0000000000..a6d4470044
--- /dev/null
+++ b/indra/newview/scripts/lua/ErrorQueue.lua
@@ -0,0 +1,30 @@
+-- ErrorQueue isa WaitQueue with the added feature that a producer can push an
+-- error through the queue. Once that error is dequeued, every consumer will
+-- raise that error.
+
+local WaitQueue = require('WaitQueue')
+
+local ErrorQueue = WaitQueue:new()
+
+function ErrorQueue:Error(message)
+ -- Setting Error() is a marker, like closing the queue. Once we reach the
+ -- error, every subsequent Dequeue() call will raise the same error.
+ self._closed = message
+ self:_wake_waiters()
+end
+
+function ErrorQueue:Dequeue()
+ local value = WaitQueue.Dequeue(self)
+ if value ~= nil then
+ -- queue not yet closed, show caller
+ return value
+ end
+ if self._closed == true then
+ -- WaitQueue:close() sets true: queue has only been closed, tell caller
+ return nil
+ end
+ -- self._closed is a message set by Error()
+ error(self._closed)
+end
+
+return ErrorQueue
diff --git a/indra/newview/scripts/lua/Queue.lua b/indra/newview/scripts/lua/Queue.lua
index e178ad9969..b0a5a87f87 100644
--- a/indra/newview/scripts/lua/Queue.lua
+++ b/indra/newview/scripts/lua/Queue.lua
@@ -1,40 +1,41 @@
--- from https://create.roblox.com/docs/luau/queues#implementing-queues
+-- from https://create.roblox.com/docs/luau/queues#implementing-queues,
+-- amended per https://www.lua.org/pil/16.1.html
local Queue = {}
-Queue.__index = Queue
-function Queue.new()
- local self = setmetatable({}, Queue)
+function Queue:new()
+ local obj = setmetatable({}, self)
+ self.__index = self
- self._first = 0
- self._last = -1
- self._queue = {}
+ obj._first = 0
+ obj._last = -1
+ obj._queue = {}
- return self
+ return obj
end
-- Check if the queue is empty
function Queue:IsEmpty()
- return self._first > self._last
+ return self._first > self._last
end
-- Add a value to the queue
function Queue:Enqueue(value)
- local last = self._last + 1
- self._last = last
- self._queue[last] = value
+ local last = self._last + 1
+ self._last = last
+ self._queue[last] = value
end
-- Remove a value from the queue
function Queue:Dequeue()
- local first = self._first
- if self:IsEmpty() then
- return nil
- end
- local value = self._queue[first]
- self._queue[first] = nil
- self._first = first + 1
- return value
+ if self:IsEmpty() then
+ return nil
+ end
+ local first = self._first
+ local value = self._queue[first]
+ self._queue[first] = nil
+ self._first = first + 1
+ return value
end
return Queue
diff --git a/indra/newview/scripts/lua/WaitQueue.lua b/indra/newview/scripts/lua/WaitQueue.lua
new file mode 100644
index 0000000000..e6adde0573
--- /dev/null
+++ b/indra/newview/scripts/lua/WaitQueue.lua
@@ -0,0 +1,80 @@
+-- WaitQueue isa Queue with the added feature that when the queue is empty,
+-- the Dequeue() operation blocks the calling coroutine until some other
+-- coroutine Enqueue()s a new value.
+
+local Queue = require('Queue')
+
+local WaitQueue = Queue:new()
+
+function WaitQueue:new()
+ local obj = Queue:new()
+ setmetatable(obj, self)
+ self.__index = self
+
+ obj._waiters = {}
+ obj._closed = false
+ return obj
+end
+
+function WaitQueue:Enqueue(value)
+ if self._closed then
+ error("can't Enqueue() on closed Queue")
+ end
+ -- can't simply call Queue:Enqueue(value)! That calls the method on the
+ -- Queue class definition, instead of calling Queue:Enqueue() on self.
+ -- Hand-expand the Queue:Enqueue() syntactic sugar.
+ Queue.Enqueue(self, value)
+ self:_wake_waiters()
+end
+
+function WaitQueue:_wake_waiters()
+ -- WaitQueue is designed to support multi-producer, multi-consumer use
+ -- cases. With multiple consumers, if more than one is trying to
+ -- Dequeue() from an empty WaitQueue, we'll have multiple waiters.
+ -- Unlike OS threads, with cooperative concurrency it doesn't make sense
+ -- to "notify all": we need resume only one of the waiting Dequeue()
+ -- callers. But since resuming that caller might entail either Enqueue()
+ -- or Dequeue() calls, recheck every time around to see if we must resume
+ -- another waiting coroutine.
+ while not self:IsEmpty() and #self._waiters > 0 do
+ -- pop the oldest waiting coroutine instead of the most recent, for
+ -- more-or-less round robin fairness
+ local waiter = table.remove(self._waiters, 1)
+ -- don't pass the head item: let the resumed coroutine retrieve it
+ local ok, message = coroutine.resume(waiter)
+ -- if resuming that waiter encountered an error, don't swallow it
+ if not ok then
+ error(message)
+ end
+ end
+end
+
+function WaitQueue:Dequeue()
+ while self:IsEmpty() do
+ -- Don't check for closed until the queue is empty: producer can close
+ -- the queue while there are still items left, and we want the
+ -- consumer(s) to retrieve those last few items.
+ if self._closed then
+ return nil
+ end
+ local coro = coroutine.running()
+ if coro == nil then
+ error("WaitQueue:Dequeue() trying to suspend main coroutine")
+ end
+ -- add the running coroutine to the list of waiters
+ table.insert(self._waiters, coro)
+ -- then let somebody else run
+ coroutine.yield()
+ end
+ -- here we're sure this queue isn't empty
+ return Queue.Dequeue(self)
+end
+
+function WaitQueue:close()
+ self._closed = true
+ -- close() is like Enqueueing an end marker. If there are waiting
+ -- consumers, give them a chance to see we're closed.
+ self:_wake_waiters()
+end
+
+return WaitQueue
diff --git a/indra/newview/scripts/lua/inspect.lua b/indra/newview/scripts/lua/inspect.lua
new file mode 100644
index 0000000000..9900a0b81b
--- /dev/null
+++ b/indra/newview/scripts/lua/inspect.lua
@@ -0,0 +1,371 @@
+local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 then local p, m = pcall(require, 'compat53.module'); if p then _tl_compat = m end end; local math = _tl_compat and _tl_compat.math or math; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table
+local inspect = {Options = {}, }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+inspect._VERSION = 'inspect.lua 3.1.0'
+inspect._URL = 'http://github.com/kikito/inspect.lua'
+inspect._DESCRIPTION = 'human-readable representations of tables'
+inspect._LICENSE = [[
+ MIT LICENSE
+
+ Copyright (c) 2022 Enrique GarcĂ­a Cota
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be included
+ in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+]]
+inspect.KEY = setmetatable({}, { __tostring = function() return 'inspect.KEY' end })
+inspect.METATABLE = setmetatable({}, { __tostring = function() return 'inspect.METATABLE' end })
+
+local tostring = tostring
+local rep = string.rep
+local match = string.match
+local char = string.char
+local gsub = string.gsub
+local fmt = string.format
+
+local _rawget
+if rawget then
+ _rawget = rawget
+else
+ _rawget = function(t, k) return t[k] end
+end
+
+local function rawpairs(t)
+ return next, t, nil
+end
+
+
+
+local function smartQuote(str)
+ if match(str, '"') and not match(str, "'") then
+ return "'" .. str .. "'"
+ end
+ return '"' .. gsub(str, '"', '\\"') .. '"'
+end
+
+
+local shortControlCharEscapes = {
+ ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n",
+ ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v", ["\127"] = "\\127",
+}
+local longControlCharEscapes = { ["\127"] = "\127" }
+for i = 0, 31 do
+ local ch = char(i)
+ if not shortControlCharEscapes[ch] then
+ shortControlCharEscapes[ch] = "\\" .. i
+ longControlCharEscapes[ch] = fmt("\\%03d", i)
+ end
+end
+
+local function escape(str)
+ return (gsub(gsub(gsub(str, "\\", "\\\\"),
+ "(%c)%f[0-9]", longControlCharEscapes),
+ "%c", shortControlCharEscapes))
+end
+
+local luaKeywords = {
+ ['and'] = true,
+ ['break'] = true,
+ ['do'] = true,
+ ['else'] = true,
+ ['elseif'] = true,
+ ['end'] = true,
+ ['false'] = true,
+ ['for'] = true,
+ ['function'] = true,
+ ['goto'] = true,
+ ['if'] = true,
+ ['in'] = true,
+ ['local'] = true,
+ ['nil'] = true,
+ ['not'] = true,
+ ['or'] = true,
+ ['repeat'] = true,
+ ['return'] = true,
+ ['then'] = true,
+ ['true'] = true,
+ ['until'] = true,
+ ['while'] = true,
+}
+
+local function isIdentifier(str)
+ return type(str) == "string" and
+ not not str:match("^[_%a][_%a%d]*$") and
+ not luaKeywords[str]
+end
+
+local flr = math.floor
+local function isSequenceKey(k, sequenceLength)
+ return type(k) == "number" and
+ flr(k) == k and
+ 1 <= (k) and
+ k <= sequenceLength
+end
+
+local defaultTypeOrders = {
+ ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4,
+ ['function'] = 5, ['userdata'] = 6, ['thread'] = 7,
+}
+
+local function sortKeys(a, b)
+ local ta, tb = type(a), type(b)
+
+
+ if ta == tb and (ta == 'string' or ta == 'number') then
+ return (a) < (b)
+ end
+
+ local dta = defaultTypeOrders[ta] or 100
+ local dtb = defaultTypeOrders[tb] or 100
+
+
+ return dta == dtb and ta < tb or dta < dtb
+end
+
+local function getKeys(t)
+
+ local seqLen = 1
+ while _rawget(t, seqLen) ~= nil do
+ seqLen = seqLen + 1
+ end
+ seqLen = seqLen - 1
+
+ local keys, keysLen = {}, 0
+ for k in rawpairs(t) do
+ if not isSequenceKey(k, seqLen) then
+ keysLen = keysLen + 1
+ keys[keysLen] = k
+ end
+ end
+ table.sort(keys, sortKeys)
+ return keys, keysLen, seqLen
+end
+
+local function countCycles(x, cycles)
+ if type(x) == "table" then
+ if cycles[x] then
+ cycles[x] = cycles[x] + 1
+ else
+ cycles[x] = 1
+ for k, v in rawpairs(x) do
+ countCycles(k, cycles)
+ countCycles(v, cycles)
+ end
+ countCycles(getmetatable(x), cycles)
+ end
+ end
+end
+
+local function makePath(path, a, b)
+ local newPath = {}
+ local len = #path
+ for i = 1, len do newPath[i] = path[i] end
+
+ newPath[len + 1] = a
+ newPath[len + 2] = b
+
+ return newPath
+end
+
+
+local function processRecursive(process,
+ item,
+ path,
+ visited)
+ if item == nil then return nil end
+ if visited[item] then return visited[item] end
+
+ local processed = process(item, path)
+ if type(processed) == "table" then
+ local processedCopy = {}
+ visited[item] = processedCopy
+ local processedKey
+
+ for k, v in rawpairs(processed) do
+ processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited)
+ if processedKey ~= nil then
+ processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited)
+ end
+ end
+
+ local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited)
+ if type(mt) ~= 'table' then mt = nil end
+ setmetatable(processedCopy, mt)
+ processed = processedCopy
+ end
+ return processed
+end
+
+local function puts(buf, str)
+ buf.n = buf.n + 1
+ buf[buf.n] = str
+end
+
+
+
+local Inspector = {}
+
+
+
+
+
+
+
+
+
+
+local Inspector_mt = { __index = Inspector }
+
+local function tabify(inspector)
+ puts(inspector.buf, inspector.newline .. rep(inspector.indent, inspector.level))
+end
+
+function Inspector:getId(v)
+ local id = self.ids[v]
+ local ids = self.ids
+ if not id then
+ local tv = type(v)
+ id = (ids[tv] or 0) + 1
+ ids[v], ids[tv] = id, id
+ end
+ return tostring(id)
+end
+
+function Inspector:putValue(v)
+ local buf = self.buf
+ local tv = type(v)
+ if tv == 'string' then
+ puts(buf, smartQuote(escape(v)))
+ elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or
+ tv == 'cdata' or tv == 'ctype' then
+ puts(buf, tostring(v))
+ elseif tv == 'table' and not self.ids[v] then
+ local t = v
+
+ if t == inspect.KEY or t == inspect.METATABLE then
+ puts(buf, tostring(t))
+ elseif self.level >= self.depth then
+ puts(buf, '{...}')
+ else
+ if self.cycles[t] > 1 then puts(buf, fmt('<%d>', self:getId(t))) end
+
+ local keys, keysLen, seqLen = getKeys(t)
+
+ puts(buf, '{')
+ self.level = self.level + 1
+
+ for i = 1, seqLen + keysLen do
+ if i > 1 then puts(buf, ',') end
+ if i <= seqLen then
+ puts(buf, ' ')
+ self:putValue(t[i])
+ else
+ local k = keys[i - seqLen]
+ tabify(self)
+ if isIdentifier(k) then
+ puts(buf, k)
+ else
+ puts(buf, "[")
+ self:putValue(k)
+ puts(buf, "]")
+ end
+ puts(buf, ' = ')
+ self:putValue(t[k])
+ end
+ end
+
+ local mt = getmetatable(t)
+ if type(mt) == 'table' then
+ if seqLen + keysLen > 0 then puts(buf, ',') end
+ tabify(self)
+ puts(buf, '<metatable> = ')
+ self:putValue(mt)
+ end
+
+ self.level = self.level - 1
+
+ if keysLen > 0 or type(mt) == 'table' then
+ tabify(self)
+ elseif seqLen > 0 then
+ puts(buf, ' ')
+ end
+
+ puts(buf, '}')
+ end
+
+ else
+ puts(buf, fmt('<%s %d>', tv, self:getId(v)))
+ end
+end
+
+
+
+
+function inspect.inspect(root, options)
+ options = options or {}
+
+ local depth = options.depth or (math.huge)
+ local newline = options.newline or '\n'
+ local indent = options.indent or ' '
+ local process = options.process
+
+ if process then
+ root = processRecursive(process, root, {}, {})
+ end
+
+ local cycles = {}
+ countCycles(root, cycles)
+
+ local inspector = setmetatable({
+ buf = { n = 0 },
+ ids = {},
+ cycles = cycles,
+ depth = depth,
+ level = 0,
+ newline = newline,
+ indent = indent,
+ }, Inspector_mt)
+
+ inspector:putValue(root)
+
+ return table.concat(inspector.buf)
+end
+
+setmetatable(inspect, {
+ __call = function(_, root, options)
+ return inspect.inspect(root, options)
+ end,
+})
+
+return inspect
diff --git a/indra/newview/scripts/lua/leap.lua b/indra/newview/scripts/lua/leap.lua
new file mode 100644
index 0000000000..708df821e8
--- /dev/null
+++ b/indra/newview/scripts/lua/leap.lua
@@ -0,0 +1,393 @@
+-- Lua implementation of LEAP (LLSD Event API Plugin) protocol
+--
+-- This module supports Lua scripts run by the Second Life viewer.
+--
+-- LEAP protocol passes LLSD objects, converted to/from Lua tables, in both
+-- directions. A typical LLSD object is a map containing keys 'pump' and
+-- 'data'.
+--
+-- The viewer's Lua post_to(pump, data) function posts 'data' to the
+-- LLEventPump 'pump'. This is typically used to engage an LLEventAPI method.
+--
+-- Similarly, the viewer gives each Lua script its own LLEventPump with a
+-- unique name. That name is returned by get_event_pumps(). Every event
+-- received on that LLEventPump is queued for retrieval by get_event_next(),
+-- which returns (pump, data): the name of the LLEventPump on which the event
+-- was received and the received event data. When the queue is empty,
+-- get_event_next() blocks the calling Lua script until the next event is
+-- received.
+
+local ErrorQueue = require('ErrorQueue')
+
+local leap = {}
+
+-- _reply: string name of reply LLEventPump. Any events the viewer posts to
+-- this pump will be queued for get_event_next(). We usually specify it as the
+-- reply pump for requests to internal viewer services.
+-- _command: string name of command LLEventPump. post_to(_command, ...)
+-- engages LLLeapListener operations such as listening on a specified other
+-- LLEventPump, etc.
+leap._reply, leap._command = get_event_pumps()
+-- Dict of features added to the LEAP protocol since baseline implementation.
+-- Before engaging a new feature that might break an older viewer, we can
+-- check for the presence of that feature key. This table is solely about the
+-- LEAP protocol itself, the way we communicate with the viewer. To discover
+-- whether a given listener exists, or supports a particular operation, use
+-- _command's "getAPI" operation.
+-- For Lua, _command's "getFeatures" operation suffices?
+-- leap._features = {}
+
+-- Each outstanding request() or generate() call has a corresponding
+-- WaitForReqid object (later in this module) to handle the
+-- response(s). If an incoming event contains an echoed ["reqid"] key,
+-- we can look up the appropriate WaitForReqid object more efficiently
+-- in a dict than by tossing such objects into the usual waitfors list.
+-- Note: the ["reqid"] must be unique, otherwise we could end up
+-- replacing an earlier WaitForReqid object in self.pending with a
+-- later one. That means that no incoming event will ever be given to
+-- the old WaitForReqid object. Any coroutine waiting on the discarded
+-- WaitForReqid object would therefore wait forever.
+leap._pending = {}
+-- Our consumer will instantiate some number of WaitFor subclass objects.
+-- As these are traversed in descending priority order, we must keep
+-- them in a list.
+leap._waitfors = {}
+-- It has been suggested that we should use UUIDs as ["reqid"] values,
+-- since UUIDs are guaranteed unique. However, as the "namespace" for
+-- ["reqid"] values is our very own _reply pump, we can get away with
+-- an integer.
+leap._reqid = 0
+
+-- get the name of the reply pump
+function leap.replypump()
+ return leap._reply
+end
+
+-- get the name of the command pump
+function leap.cmdpump()
+ return leap._command
+end
+
+-- Fire and forget. Send the specified request LLSD, expecting no reply.
+-- In fact, should the request produce an eventual reply, it will be
+-- treated as an unsolicited event.
+--
+-- See also request(), generate().
+function leap.send(pump, data, reqid)
+ local data = data
+ if type(data) == 'table' then
+ data = table.clone(data)
+ data['reply'] = leap._reply
+ if reqid ~= nil then
+ data['reqid'] = reqid
+ end
+ end
+ post_to(pump, data)
+end
+
+-- Send the specified request LLSD, expecting exactly one reply. Block
+-- the calling coroutine until we receive that reply.
+--
+-- Every request() (or generate()) LLSD block we send will get stamped
+-- with a distinct ["reqid"] value. The requested event API must echo the
+-- same ["reqid"] field in each reply associated with that request. This way
+-- we can correctly dispatch interleaved replies from different requests.
+--
+-- If the desired event API doesn't support the ["reqid"] echo convention,
+-- you should use send() instead -- since request() or generate() would
+-- wait forever for a reply stamped with that ["reqid"] -- and intercept
+-- any replies using WaitFor.
+--
+-- Unless the request data already contains a ["reply"] key, we insert
+-- reply=self.replypump to try to ensure that the expected reply will be
+-- returned over the socket.
+function leap.request(pump, data)
+ local reqid = leap._requestSetup(pump, data)
+ local ok, response = pcall(leap._pending[reqid].wait)
+ -- kill off temporary WaitForReqid object, even if error
+ leap._pending[reqid] = nil
+ if ok then
+ return response
+ else
+ error(response)
+ end
+end
+
+-- common setup code shared by request() and generate()
+function leap._requestSetup(pump, data)
+ -- invent a new, unique reqid
+ local reqid = leap._reqid
+ leap._reqid += 1
+ -- Instantiate a new WaitForReqid object. The priority is irrelevant
+ -- because, unlike the WaitFor base class, WaitForReqid does not
+ -- self-register on our leap._waitfors list. Instead, capture the new
+ -- WaitForReqid object in leap._pending so _dispatch() can find it.
+ leap._pending[reqid] = leap.WaitForReqid:new(reqid)
+ -- Pass reqid to send() to stamp it into (a copy of) the request data.
+ leap.send(pump, data, reqid)
+ return reqid
+end
+
+-- Send the specified request LLSD, expecting an arbitrary number of replies.
+-- Each one is yielded on receipt. If you omit checklast, this is an infinite
+-- generator; it's up to the caller to recognize when the last reply has been
+-- received, and stop resuming for more.
+--
+-- If you pass checklast=<callable accepting(event)>, each response event is
+-- passed to that callable (after the yield). When the callable returns
+-- True, the generator terminates in the usual way.
+--
+-- See request() remarks about ["reqid"].
+function leap.generate(pump, data, checklast)
+ -- Invent a new, unique reqid. Arrange to handle incoming events
+ -- bearing that reqid. Stamp the outbound request with that reqid, and
+ -- send it.
+ local reqid = leap._requestSetup(pump, data)
+ local ok, response
+ repeat
+ ok, response = pcall(leap._pending[reqid].wait)
+ if not ok then
+ break
+ end
+ coroutine.yield(response)
+ until checklast and checklast(response)
+ -- If we break the above loop, whether or not due to error, clean up.
+ leap._pending[reqid] = nil
+ if not ok then
+ error(response)
+ end
+end
+
+-- Kick off response processing. The calling script must create and resume one
+-- or more coroutines to perform viewer requests using send(), request() or
+-- generate() before calling this function to handle responses.
+--
+-- While waiting for responses from the viewer, the C++ coroutine running the
+-- calling Lua script is blocked: no other Lua coroutine is running.
+function leap.process()
+ local ok, pump, data
+ while true do
+ ok, pump, data = pcall(get_event_next)
+ if not ok then
+ break
+ end
+ leap._dispatch(pump, data)
+ end
+ -- we're done: clean up all pending coroutines
+ for i, waitfor in pairs(leap._pending) do
+ waitfor._exception(pump)
+ end
+ for i, waitfor in pairs(leap._waitfors) do
+ waitfor._exception(pump)
+ end
+ -- now that we're done with cleanup, propagate the error we caught above
+ if not ok then
+ error(pump)
+ end
+end
+
+-- Route incoming (pump, data) event to the appropriate waiting coroutine.
+function leap._dispatch(pump, data)
+ local reqid = data['reqid']
+ -- if the response has no 'reqid', it's not from request() or generate()
+ if reqid == nil then
+ return leap._unsolicited(pump, data)
+ end
+ -- have reqid; do we have a WaitForReqid?
+ local waitfor = leap._pending[reqid]
+ if waitfor == nil then
+ return leap._unsolicited(pump, data)
+ end
+ -- found the right WaitForReqid object, let it handle the event
+ data['reqid'] = nil
+ waitfor._handle(pump, data)
+end
+
+-- Handle an incoming (pump, data) event with no recognizable ['reqid']
+function leap._unsolicited(pump, data)
+ -- we maintain waitfors in descending priority order, so the first waitfor
+ -- to claim this event is the one with the highest priority
+ for i, waitfor in pairs(leap._waitfors) do
+ if waitfor._handle(pump, data) then
+ return
+ end
+ end
+ print_debug('_unsolicited(', pump, ', ', data, ') discarding unclaimed event')
+end
+
+-- called by WaitFor.enable()
+function leap._registerWaitFor(waitfor)
+ table.insert(leap._waitfors, waitfor)
+ -- keep waitfors sorted in descending order of specified priority
+ table.sort(leap._waitfors,
+ function (lhs, rhs) return lhs.priority > rhs.priority end)
+end
+
+-- called by WaitFor.disable()
+function leap._unregisterWaitFor(waitfor)
+ for i, w in pairs(leap._waitfors) do
+ if w == waitfor then
+ leap._waitfors[i] = nil
+ break
+ end
+ end
+end
+
+-- ******************************************************************************
+-- WaitFor and friends
+-- ******************************************************************************
+
+-- An unsolicited event is handled by the highest-priority WaitFor subclass
+-- object willing to accept it. If no such object is found, the unsolicited
+-- event is discarded.
+--
+-- * First, instantiate a WaitFor subclass object to register its interest in
+-- some incoming event(s). WaitFor instances are self-registering; merely
+-- instantiating the object suffices.
+-- * Any coroutine may call a given WaitFor object's wait() method. This blocks
+-- the calling coroutine until a suitable event arrives.
+-- * WaitFor's constructor accepts a float priority. Every incoming event
+-- (other than those claimed by request() or generate()) is passed to each
+-- extant WaitFor.filter() method in descending priority order. The first
+-- such filter() to return nontrivial data claims that event.
+-- * At that point, the blocked wait() call on that WaitFor object returns the
+-- item returned by filter().
+-- * WaitFor contains a queue. Multiple arriving events claimed by that WaitFor
+-- object's filter() method are added to the queue. Naturally, until the
+-- queue is empty, calling wait() immediately returns the front entry.
+--
+-- It's reasonable to instantiate a WaitFor subclass whose filter() method
+-- unconditionally returns the incoming event, and whose priority places it
+-- last in the list. This object will enqueue every unsolicited event left
+-- unclaimed by other WaitFor subclass objects.
+--
+-- It's not strictly necessary to associate a WaitFor object with exactly one
+-- coroutine. You might have multiple "worker" coroutines drawing from the same
+-- WaitFor object, useful if the work being done per event might itself involve
+-- "blocking" operations. Or a given coroutine might sample a number of WaitFor
+-- objects in round-robin fashion... etc. etc. Nonetheless, it's
+-- straightforward to designate one coroutine for each WaitFor object.
+
+-- --------------------------------- WaitFor ---------------------------------
+leap.WaitFor = { _id=0 }
+
+function leap.WaitFor:new(priority, name)
+ local obj = setmetatable({}, self)
+ self.__index = self
+
+ obj.priority = priority
+ if name then
+ obj.name = name
+ else
+ self._id += 1
+ obj.name = 'WaitFor' .. self._id
+ end
+ obj._queue = ErrorQueue:new()
+ obj._registered = false
+ obj:enable()
+
+ return obj
+end
+
+function leap.WaitFor.tostring(self)
+ -- Lua (sub)classes have no name; can't prefix with that
+ return self.name
+end
+
+-- Re-enable a disable()d WaitFor object. New WaitFor objects are
+-- enable()d by default.
+function leap.WaitFor:enable()
+ if not self._registered then
+ leap._registerWaitFor(self)
+ self._registered = true
+ end
+end
+
+-- Disable an enable()d WaitFor object.
+function leap.WaitFor:disable()
+ if self._registered then
+ leap._unregisterWaitFor(self)
+ self._registered = false
+ end
+end
+
+-- Block the calling coroutine until a suitable unsolicited event (one
+-- for which filter() returns the event) arrives.
+function leap.WaitFor:wait()
+ return self._queue:Dequeue()
+end
+
+-- Loop over wait() calls.
+function leap.WaitFor:iterate()
+ -- on each iteration, call self.wait(self)
+ return self.wait, self, nil
+end
+
+-- Override filter() to examine the incoming event in whatever way
+-- makes sense.
+--
+-- Return nil to ignore this event.
+--
+-- To claim the event, return the item you want placed in the queue.
+-- Typically you'd write:
+-- return data
+-- or perhaps
+-- return {pump=pump, data=data}
+-- or some variation.
+function leap.WaitFor:filter(pump, data)
+ error('You must subclass WaitFor and override its filter() method')
+end
+
+-- called by leap._unsolicited() for each WaitFor in leap._waitfors
+function leap.WaitFor:_handle(pump, data)
+ item = self:filter(pump, data)
+ -- if this item doesn't pass the filter, we're not interested
+ if not item then
+ return false
+ end
+ -- okay, filter() claims this event
+ self:process(item)
+ return true
+end
+
+-- called by WaitFor:_handle() for an accepted event
+function leap.WaitFor:process(item)
+ self._queue:Enqueue(item)
+end
+
+-- called by leap.process() when get_event_next() raises an error
+function leap.WaitFor:_exception(message)
+ self._queue:Error(message)
+end
+
+-- ------------------------------ WaitForReqid -------------------------------
+leap.WaitForReqid = leap.WaitFor:new()
+
+function leap.WaitForReqid:new(reqid)
+ -- priority is meaningless, since this object won't be added to the
+ -- priority-sorted ViewerClient.waitfors list. Use the reqid as the
+ -- debugging name string.
+ local obj = leap.WaitFor:new(0, 'WaitForReqid(' .. reqid .. ')')
+ setmetatable(obj, self)
+ self.__index = self
+
+ return obj
+end
+
+function leap.WaitForReqid:enable()
+ -- Do NOT self-register in the normal way. request() and generate()
+ -- have an entirely different registry that points directly to the
+ -- WaitForReqid object of interest.
+end
+
+function leap.WaitForReqid:disable()
+end
+
+function leap.WaitForReqid:filter(pump, data)
+ -- Because we expect to directly look up the WaitForReqid object of
+ -- interest based on the incoming ["reqid"] value, it's not necessary
+ -- to test the event again. Accept every such event.
+ return data
+end
+
+return leap
diff --git a/indra/newview/scripts/lua/qtest.lua b/indra/newview/scripts/lua/qtest.lua
new file mode 100644
index 0000000000..12e425b7b2
--- /dev/null
+++ b/indra/newview/scripts/lua/qtest.lua
@@ -0,0 +1,131 @@
+-- Exercise the Queue, WaitQueue, ErrorQueue family
+
+Queue = require('Queue')
+WaitQueue = require('WaitQueue')
+ErrorQueue = require('ErrorQueue')
+util = require('util')
+
+inspect = require('inspect')
+
+-- ------------------ Queue variables are instance-specific ------------------
+q1 = Queue:new()
+q2 = Queue:new()
+
+q1:Enqueue(17)
+
+assert(not q1:IsEmpty())
+assert(q2:IsEmpty())
+assert(q1:Dequeue() == 17)
+assert(q1:Dequeue() == nil)
+assert(q2:Dequeue() == nil)
+
+-- ----------------------------- test WaitQueue ------------------------------
+q1 = WaitQueue:new()
+q2 = WaitQueue:new()
+result = {}
+values = { 1, 1, 2, 3, 5, 8, 13, 21 }
+
+for i, value in pairs(values) do
+ q1:Enqueue(value)
+end
+-- close() while not empty tests that queue drains before reporting done
+q1:close()
+
+-- ensure that WaitQueue instance variables are in fact independent
+assert(q2:IsEmpty())
+
+-- consumer() coroutine to pull from the passed q until closed
+function consumer(desc, q)
+ print(string.format('consumer(%s) start', desc))
+ local value = q:Dequeue()
+ while value ~= nil do
+ print(string.format('consumer(%s) got %q', desc, value))
+ table.insert(result, value)
+ value = q:Dequeue()
+ end
+ print(string.format('consumer(%s) done', desc))
+end
+
+-- run two consumers
+coa = coroutine.create(consumer)
+cob = coroutine.create(consumer)
+-- Since consumer() doesn't yield while it can still retrieve values,
+-- consumer(a) will dequeue all values from q1 and return when done.
+coroutine.resume(coa, 'a', q1)
+-- consumer(b) will wake up to find the queue empty and closed.
+coroutine.resume(cob, 'b', q1)
+coroutine.close(coa)
+coroutine.close(cob)
+
+print('values:', inspect(values))
+print('result:', inspect(result))
+
+assert(util.equal(values, result))
+
+-- try incrementally enqueueing values
+q3 = WaitQueue:new()
+result = {}
+values = { 'This', 'is', 'a', 'test', 'script' }
+
+coros = {}
+for _, name in {'a', 'b'} do
+ local coro = coroutine.create(consumer)
+ table.insert(coros, coro)
+ -- Resuming both coroutines should leave them both waiting for a queue item.
+ coroutine.resume(coro, name, q3)
+end
+
+for _, s in pairs(values) do
+ print(string.format('Enqueue(%q)', s))
+ q3:Enqueue(s)
+end
+q3:close()
+
+function joinall(coros)
+ local running
+ local errors = 0
+ repeat
+ running = false
+ for i, coro in pairs(coros) do
+ if coroutine.status(coro) == 'suspended' then
+ running = true
+ local ok, message = coroutine.resume(coro)
+ if not ok then
+ print('*** ' .. message)
+ errors += 1
+ end
+ if coroutine.status(coro) == 'dead' then
+ coros[i] = nil
+ end
+ end
+ end
+ until not running
+ return errors
+end
+
+joinall(coros)
+
+print(string.format('%q', util.join(result, ' ')))
+assert(util.equal(values, result))
+
+-- ----------------------------- test ErrorQueue -----------------------------
+q4 = ErrorQueue:new()
+result = {}
+values = { 'This', 'is', 'a', 'test', 'script' }
+
+coros = {}
+for _, name in {'a', 'b'} do
+ local coro = coroutine.create(consumer)
+ table.insert(coros, coro)
+ -- Resuming both coroutines should leave them both waiting for a queue item.
+ coroutine.resume(coro, name, q4)
+end
+
+for i = 1, 4 do
+ print(string.format('Enqueue(%q)', values[i]))
+ q4:Enqueue(values[i])
+end
+q4:Error('something went wrong')
+
+assert(joinall(coros) == 2)
+
diff --git a/indra/newview/scripts/lua/util.lua b/indra/newview/scripts/lua/util.lua
new file mode 100644
index 0000000000..bb8d492d12
--- /dev/null
+++ b/indra/newview/scripts/lua/util.lua
@@ -0,0 +1,72 @@
+-- utility functions, in alpha order
+
+local util = {}
+
+-- cheap test whether table t is empty
+function util.empty(t)
+ for _ in pairs(t) do
+ return false
+ end
+ return true
+end
+
+-- reliable count of the number of entries in table t
+-- (since #t is unreliable)
+function util.count(t)
+ local count = 0
+ for _ in pairs(t) do
+ count += 1
+ end
+ return count
+end
+
+-- recursive table equality
+function util.equal(t1, t2)
+ if not (type(t1) == 'table' and type(t2) == 'table') then
+ return t1 == t2
+ end
+ -- both t1 and t2 are tables: get modifiable copy of t2
+ local temp = table.clone(t2)
+ for k, v in pairs(t1) do
+ -- if any key in t1 doesn't have same value in t2, not equal
+ if not util.equal(v, temp[k]) then
+ return false
+ end
+ -- temp[k] == t1[k], delete temp[k]
+ temp[k] = nil
+ end
+ -- All keys in t1 have equal values in t2; t2 == t1 if there are no extra keys in t2
+ return util.empty(temp)
+end
+
+-- Concatentate the strings in the passed list, return the composite string.
+-- For iterative string building, the theory is that building a list with
+-- table.insert() and then using join() to allocate the full-size result
+-- string once should be more efficient than reallocating an intermediate
+-- string for every partial concatenation.
+function util.join(list, sep)
+ -- This succinct implementation assumes that string.format() precomputes
+ -- the required size of its output buffer before populating it. We don't
+ -- know that. Moreover, this implementation predates our sep argument.
+-- return string.format(string.rep('%s', #list), table.unpack(list))
+
+ -- this implementation makes it explicit
+ local sep = sep or ''
+ local size = if util.empty(list) then 0 else -#sep
+ for _, s in pairs(list) do
+ size += #sep + #s
+ end
+ local result = buffer.create(size)
+ size = 0
+ for i, s in pairs(list) do
+ if i > 1 then
+ buffer.writestring(result, size, sep)
+ size += #sep
+ end
+ buffer.writestring(result, size, s)
+ size += #s
+ end
+ return buffer.tostring(result)
+end
+
+return util
diff --git a/indra/newview/tests/llluamanager_test.cpp b/indra/newview/tests/llluamanager_test.cpp
index 81ec186cf4..1c2be900d3 100644
--- a/indra/newview/tests/llluamanager_test.cpp
+++ b/indra/newview/tests/llluamanager_test.cpp
@@ -94,9 +94,10 @@ namespace tut
{
auto [count, result] =
LLLUAmanager::waitScriptLine(L, "return " + luax.expr);
- auto desc{ stringize("waitScriptLine(", luax.desc, ")") };
- ensure_equals(desc + ".count", count, 1);
- ensure_equals(desc + ".result", result, luax.expect);
+ auto desc{ stringize("waitScriptLine(", luax.desc, "): ") };
+ // if count < 0, report Lua error message
+ ensure_equals(desc + result.asString(), count, 1);
+ ensure_equals(desc + "result", result, luax.expect);
}
}
@@ -116,7 +117,7 @@ namespace tut
// We woke up again ourselves because the coroutine running Lua has
// finished. But our Lua chunk didn't actually return anything, so we
// expect count to be 0 and result to be undefined.
- ensure_equals(desc + " count", count, 0);
+ ensure_equals(desc + ": " + result.asString(), count, 0);
ensure_equals(desc, fromlua, expect);
}
@@ -172,16 +173,13 @@ namespace tut
<< "': post('" << expected[4] << "')" << LL_ENDL;
luapump.post(expected[4]);
auto [count, result] = future.get();
+ ensure_equals("post_on(): " + result.asString(), count, 0);
ensure_equals("post_on() sequence", posts, expected);
}
void round_trip(const std::string& desc, const LLSD& send, const LLSD& expect)
{
- LLSD reply;
- LLEventStream replypump("testpump");
- LLTempBoundListener conn(
- replypump.listen("llluamanager_test",
- listener([&reply](const LLSD& post){ reply = post; })));
+ LLEventMailDrop replypump("testpump");
const std::string lua(
"-- test LLSD round trip\n"
"replypump, cmdpump = get_event_pumps()\n"
@@ -195,12 +193,12 @@ namespace tut
// reached the get_event_next() call, which suspends the calling C++
// coroutine (including the Lua code running on it) until we post
// something to that reply pump.
- auto luapump{ reply.asString() };
- reply.clear();
+ auto luapump{ llcoro::suspendUntilEventOn(replypump).asString() };
LLEventPumps::instance().post(luapump, send);
// The C++ coroutine running the Lua script is now ready to run. Run
// it so it will echo the LLSD back to us.
auto [count, result] = future.get();
+ ensure_equals(stringize("round_trip(", desc, "): ", result.asString()), count, 1);
ensure_equals(desc, result, expect);
}
@@ -304,4 +302,18 @@ namespace tut
}
round_trip("nested map", send_map, expect_map);
}
+
+ template<> template<>
+ void object::test<5>()
+ {
+ set_test_name("test leap.lua");
+ const std::string lua(
+ "-- test leap.lua\n"
+ "leap = require('leap')\n"
+ );
+ LuaState L;
+ auto future = LLLUAmanager::startScriptLine(L, lua);
+ auto [count, result] = future.get();
+ ensure_equals("leap.lua: " + result.asString(), count, 0);
+ }
} // namespace tut