From 486a6b189a3ea3fb2700718a64f574c3240fae7d Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Fri, 7 Jun 2024 21:43:41 -0400 Subject: Introduce mapargs.lua, which defines the mapargs() function. There are two conventions for Lua function calls. You can call a function with positional arguments as usual: f(1, 2, 3) Lua makes it easy to handle omitted positional arguments: their values are nil. But as in C++, positional arguments get harder to read when there are many, or when you want to omit arguments other than the last ones. Alternatively, using Lua syntactic sugar, you can pass a single argument which is a table containing the desired function arguments. For this you can use table constructor syntax to effect keyword arguments: f{a=1, b=2, c=3} A call passing keyword arguments is more readable because you explicitly associate the parameter name with each argument value. Moreover, it gracefully handles the case of multiple optional arguments. The reader need not be concerned about parameters *not* being passed. Now you're coding a Lua module with a number of functions. Some have numerous or complicated arguments; some do not. For simplicity, you code the simple functions to accept positional arguments, the more complicated functions to accept the single-table argument style. But how the bleep is a consumer of your module supposed to remember which calling style to use for a given function? mapargs() blurs the distinction, accepting either style. Coding a function like this (where '...' is literal code, not documentation ellipsis): function f(...) local args = mapargs({'a', 'b', 'c'}, ...) -- now use args.a, args.b, args.c end supports calls like: f(1, 2, 3) f{1, 2, 3} f{c=3, a=1, b=2} f{1, 2, c=3} f{c=3, 1, 2} -- unlike Python! In every call above, args.a == 1, args.b == 2, args.c == 3. Moreover, omitting arguments (or explicitly passing nil, positionally or by keyword) works correctly. test_mapargs.lua exercises these cases. --- indra/newview/scripts/lua/mapargs.lua | 67 +++++++++++++++++++++++++++++ indra/newview/scripts/lua/test_mapargs.lua | 68 ++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 indra/newview/scripts/lua/mapargs.lua create mode 100644 indra/newview/scripts/lua/test_mapargs.lua diff --git a/indra/newview/scripts/lua/mapargs.lua b/indra/newview/scripts/lua/mapargs.lua new file mode 100644 index 0000000000..78e691d8bc --- /dev/null +++ b/indra/newview/scripts/lua/mapargs.lua @@ -0,0 +1,67 @@ +-- Allow a calling function to be passed a mix of positional arguments with +-- keyword arguments. Reference them as fields of a table. +-- Don't use this for a function that can accept a single table argument. +-- mapargs() assumes that a single table argument means its caller was called +-- with f{table constructor} syntax, and maps that table to the specified names. +-- Usage: +-- function f(...) +-- local a = mapargs({'a1', 'a2', 'a3'}, ...) +-- ... a.a1 ... etc. +-- end +-- f(10, 20, 30) -- a.a1 == 10, a.a2 == 20, a.a3 == 30 +-- f{10, 20, 30} -- a.a1 == 10, a.a2 == 20, a.a3 == 30 +-- f{a3=300, a1=100} -- a.a1 == 100, a.a2 == nil, a.a3 == 300 +-- f{1, a3=3} -- a.a1 == 1, a.a2 == nil, a.a3 == 3 +-- f{a3=3, 1} -- a.a1 == 1, a.a2 == nil, a.a3 == 3 +local function mapargs(names, ...) + local args = table.pack(...) + local posargs = {} + local keyargs = {} + -- For a mixed table, no Lua operation will reliably tell you how many + -- array items it contains, if there are any holes. Track that by hand. + -- We must be able to handle f(1, nil, 3) calls. + local maxpos = 0 + if not (args.n == 1 and type(args[1]) == 'table') then + -- If caller passes more than one argument, or if the first argument + -- is not a table, then it's classic positional function-call syntax: + -- f(first, second, etc.). In that case we need not bother teasing + -- apart positional from keyword arguments. + posargs = args + maxpos = args.n + else + -- Single table argument implies f{mixed} syntax. + -- Tease apart positional arguments from keyword arguments. + for k, v in pairs(args[1]) do + if type(k) == 'number' then + posargs[k] = v + maxpos = math.max(maxpos, k) + else + if table.find(names, k) == nil then + error('unknown keyword argument ' .. tostring(k)) + end + keyargs[k] = v + end + end + end + + -- keyargs already has keyword arguments in place, just fill in positionals + args = keyargs + -- Don't exceed the number of parameter names. Loop explicitly over every + -- index value instead of using ipairs() so we can support holes (nils) in + -- posargs. + for i = 1, math.min(#names, maxpos) do + if posargs[i] ~= nil then + -- As in Python, make it illegal to pass an argument both positionally + -- and by keyword. This implementation permits func(17, first=nil), a + -- corner case about which I don't particularly care. + if args[names[i]] ~= nil then + error(string.format('parameter %s passed both positionally and by keyword', + tostring(names[i]))) + end + args[names[i]] = posargs[i] + end + end + return args +end + +return mapargs diff --git a/indra/newview/scripts/lua/test_mapargs.lua b/indra/newview/scripts/lua/test_mapargs.lua new file mode 100644 index 0000000000..999a57acb4 --- /dev/null +++ b/indra/newview/scripts/lua/test_mapargs.lua @@ -0,0 +1,68 @@ +local mapargs = require 'mapargs' +local inspect = require 'inspect' + +function tabfunc(...) + local a = mapargs({'a1', 'a2', 'a3'}, ...) + print(inspect(a)) +end + +print('----------') +print('f(10, 20, 30)') +tabfunc(10, 20, 30) +print('f(10, nil, 30)') +tabfunc(10, nil, 30) +print('f{10, 20, 30}') +tabfunc{10, 20, 30} +print('f{10, nil, 30}') +tabfunc{10, nil, 30} +print('f{a3=300, a1=100}') +tabfunc{a3=300, a1=100} +print('f{1, a3=3}') +tabfunc{1, a3=3} +print('f{a3=3, 1}') +tabfunc{a3=3, 1} +print('----------') + +if false then + -- the code below was used to explore ideas that became mapargs() + mixed = { '[1]', nil, '[3]', abc='[abc]', '[3]', def='[def]' } + local function showtable(desc, t) + print(string.format('%s (len %s)\n%s', desc, #t, inspect(t))) + end + showtable('mixed', mixed) + + print('ipairs(mixed)') + for k, v in ipairs(mixed) do + print(string.format('[%s] = %s', k, tostring(v))) + end + + print('table.pack(mixed)') + print(inspect(table.pack(mixed))) + + local function nilarg(desc, a, b, c) + print(desc) + print('a = ' .. tostring(a)) + print('b = ' .. tostring(b)) + print('c = ' .. tostring(c)) + end + + nilarg('nilarg(1)', 1) + nilarg('nilarg(1, nil, 3)', 1, nil, 3) + + local function nilargs(desc, ...) + args = table.pack(...) + showtable(desc, args) + end + + nilargs('nilargs{a=1, b=2, c=3}', {a=1, b=2, c=3}) + nilargs('nilargs(1, 2, 3)', 1, 2, 3) + nilargs('nilargs(1, nil, 3)', 1, nil, 3) + nilargs('nilargs{1, 2, 3}', {1, 2, 3}) + nilargs('nilargs{1, nil, 3}', {1, nil, 3}) + + print('table.unpack({1, nil, 3})') + a, b, c = table.unpack({1, nil, 3}) + print('a = ' .. tostring(a)) + print('b = ' .. tostring(b)) + print('c = ' .. tostring(c)) +end -- cgit v1.2.3 From 9b7d71cdfc09e9d49a2bbd44a010086b37ab78b4 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 11 Jun 2024 13:37:59 -0400 Subject: Update "LLWindow" listener doc to cite github URL, not bitbucket. --- indra/newview/llwindowlistener.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indra/newview/llwindowlistener.cpp b/indra/newview/llwindowlistener.cpp index 87d18cfc00..efe395d9ca 100644 --- a/indra/newview/llwindowlistener.cpp +++ b/indra/newview/llwindowlistener.cpp @@ -55,7 +55,7 @@ LLWindowListener::LLWindowListener(LLViewerWindow *window, const KeyboardGetter& "Given [\"keysym\"], [\"keycode\"] or [\"char\"], inject the specified "; std::string keyExplain = "(integer keycode values, or keysym string from any addKeyName() call in\n" - "http://bitbucket.org/lindenlab/viewer-release/src/tip/indra/llwindow/llkeyboard.cpp )\n"; + "https://github.com/secondlife/viewer/blob/main/indra/llwindow/llkeyboard.cpp#L68-L124)\n"; std::string mask = "Specify optional [\"mask\"] as an array containing any of \"CTL\", \"ALT\",\n" "\"SHIFT\" or \"MAC_CONTROL\"; the corresponding modifier bits will be combined\n" -- cgit v1.2.3 From eae45eefb55410782559b4ace5350b2a99f63234 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 11 Jun 2024 13:39:13 -0400 Subject: mapargs() now accepts 'name1,name2,...' as argument names in addition to a list {'name1', 'name2', ...}. --- indra/newview/scripts/lua/mapargs.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/indra/newview/scripts/lua/mapargs.lua b/indra/newview/scripts/lua/mapargs.lua index 78e691d8bc..45f5a9c556 100644 --- a/indra/newview/scripts/lua/mapargs.lua +++ b/indra/newview/scripts/lua/mapargs.lua @@ -21,6 +21,12 @@ local function mapargs(names, ...) -- array items it contains, if there are any holes. Track that by hand. -- We must be able to handle f(1, nil, 3) calls. local maxpos = 0 + + -- For convenience, allow passing 'names' as a string 'n0,n1,...' + if type(names) == 'string' then + names = string.split(names, ',') + end + if not (args.n == 1 and type(args[1]) == 'table') then -- If caller passes more than one argument, or if the first argument -- is not a table, then it's classic positional function-call syntax: -- cgit v1.2.3 From 18c4dcc5998e061fe3ab54607665c775dd18c826 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 11 Jun 2024 21:42:10 -0400 Subject: Allow Python-like 'object = ClassName(ctor args)' constructor calls. The discussions we've read about Lua classes conventionally use ClassName:new() as the constructor, and so far we've followed that convention. But setting metaclass(ClassName).__call = ClassName.new permits Lua to respond to calls of the form ClassName(ctor args) by implicitly calling ClassName:new(ctor args). Introduce util.classctor(). Calling util.classctor(ClassName) sets ClassName's metaclass's __call to ClassName's constructor method. If the constructor method is named something other than new(), pass ClassName.method as the second arg. Use util.classctor() on each of our classes that defines a new() method. Replace ClassName:new(args) calls with ClassName(args) calls throughout. --- indra/newview/scripts/lua/ErrorQueue.lua | 5 ++++- indra/newview/scripts/lua/Floater.lua | 3 +++ indra/newview/scripts/lua/Queue.lua | 4 ++++ indra/newview/scripts/lua/WaitQueue.lua | 7 ++++-- indra/newview/scripts/lua/leap.lua | 13 +++++++---- indra/newview/scripts/lua/qtest.lua | 12 +++++------ indra/newview/scripts/lua/test_luafloater_demo.lua | 2 +- .../newview/scripts/lua/test_luafloater_demo2.lua | 2 +- .../scripts/lua/test_luafloater_gesture_list.lua | 2 +- .../scripts/lua/test_luafloater_gesture_list2.lua | 2 +- indra/newview/scripts/lua/test_timers.lua | 16 +++++++------- indra/newview/scripts/lua/timers.lua | 3 +++ indra/newview/scripts/lua/util.lua | 25 ++++++++++++++++++++++ 13 files changed, 71 insertions(+), 25 deletions(-) diff --git a/indra/newview/scripts/lua/ErrorQueue.lua b/indra/newview/scripts/lua/ErrorQueue.lua index 13e4e92941..e6e9a5ef48 100644 --- a/indra/newview/scripts/lua/ErrorQueue.lua +++ b/indra/newview/scripts/lua/ErrorQueue.lua @@ -5,8 +5,11 @@ local WaitQueue = require('WaitQueue') local function dbg(...) end -- local dbg = require('printf') +local util = require('util') -local ErrorQueue = WaitQueue:new() +local ErrorQueue = WaitQueue() + +util.classctor(ErrorQueue) function ErrorQueue:Error(message) -- Setting Error() is a marker, like closing the queue. Once we reach the diff --git a/indra/newview/scripts/lua/Floater.lua b/indra/newview/scripts/lua/Floater.lua index 75696533e4..d057a74386 100644 --- a/indra/newview/scripts/lua/Floater.lua +++ b/indra/newview/scripts/lua/Floater.lua @@ -2,6 +2,7 @@ local leap = require 'leap' local fiber = require 'fiber' +local util = require 'util' -- list of all the events that a LLLuaFloater might send local event_list = leap.request("LLFloaterReg", {op="getFloaterEvents"}).events @@ -45,6 +46,8 @@ function Floater:new(path, extra) return obj end +util.classctor(Floater) + function Floater:show() -- leap.eventstream() returns the first response, and launches a -- background fiber to call the passed callback with all subsequent diff --git a/indra/newview/scripts/lua/Queue.lua b/indra/newview/scripts/lua/Queue.lua index 5ab2a8a72c..5bc72e4057 100644 --- a/indra/newview/scripts/lua/Queue.lua +++ b/indra/newview/scripts/lua/Queue.lua @@ -7,6 +7,8 @@ -- But had to resist -- For fear it might be too obscua. +local util = require 'util' + local Queue = {} function Queue:new() @@ -20,6 +22,8 @@ function Queue:new() return obj end +util.classctor(Queue) + -- Check if the queue is empty function Queue:IsEmpty() return self._first > self._last diff --git a/indra/newview/scripts/lua/WaitQueue.lua b/indra/newview/scripts/lua/WaitQueue.lua index 6bcb9d62c2..7e10d03295 100644 --- a/indra/newview/scripts/lua/WaitQueue.lua +++ b/indra/newview/scripts/lua/WaitQueue.lua @@ -4,14 +4,15 @@ local fiber = require('fiber') local Queue = require('Queue') +local util = require('util') local function dbg(...) end -- local dbg = require('printf') -local WaitQueue = Queue:new() +local WaitQueue = Queue() function WaitQueue:new() - local obj = Queue:new() + local obj = Queue() setmetatable(obj, self) self.__index = self @@ -20,6 +21,8 @@ function WaitQueue:new() return obj end +util.classctor(WaitQueue) + function WaitQueue:Enqueue(value) if self._closed then error("can't Enqueue() on closed Queue") diff --git a/indra/newview/scripts/lua/leap.lua b/indra/newview/scripts/lua/leap.lua index 8caae24e94..82f91ce9e9 100644 --- a/indra/newview/scripts/lua/leap.lua +++ b/indra/newview/scripts/lua/leap.lua @@ -43,6 +43,7 @@ local ErrorQueue = require('ErrorQueue') local inspect = require('inspect') local function dbg(...) end -- local dbg = require('printf') +local util = require('util') local leap = {} @@ -129,7 +130,7 @@ local function requestSetup(pump, data) -- because, unlike the WaitFor base class, WaitForReqid does not -- self-register on our waitfors list. Instead, capture the new -- WaitForReqid object in pending so dispatch() can find it. - local waitfor = leap.WaitForReqid:new(reqid) + local waitfor = leap.WaitForReqid(reqid) pending[reqid] = waitfor -- Pass reqid to send() to stamp it into (a copy of) the request data. dbg('requestSetup(%s, %s) storing %s', pump, data, waitfor.name) @@ -432,7 +433,7 @@ function leap.WaitFor:new(priority, name) self._id += 1 obj.name = 'WaitFor' .. self._id end - obj._queue = ErrorQueue:new() + obj._queue = ErrorQueue() obj._registered = false -- if no priority, then don't enable() - remember 0 is truthy if priority then @@ -442,6 +443,8 @@ function leap.WaitFor:new(priority, name) return obj end +util.classctor(leap.WaitFor) + -- Re-enable a disable()d WaitFor object. New WaitFor objects are -- enable()d by default. function leap.WaitFor:enable() @@ -514,13 +517,13 @@ function leap.WaitFor:exception(message) end -- ------------------------------ WaitForReqid ------------------------------- -leap.WaitForReqid = leap.WaitFor:new() +leap.WaitForReqid = leap.WaitFor() function leap.WaitForReqid:new(reqid) -- priority is meaningless, since this object won't be added to the -- priority-sorted waitfors list. Use the reqid as the debugging name -- string. - local obj = leap.WaitFor:new(nil, 'WaitForReqid(' .. reqid .. ')') + local obj = leap.WaitFor(nil, 'WaitForReqid(' .. reqid .. ')') setmetatable(obj, self) self.__index = self @@ -529,6 +532,8 @@ function leap.WaitForReqid:new(reqid) return obj end +util.classctor(leap.WaitForReqid) + 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 diff --git a/indra/newview/scripts/lua/qtest.lua b/indra/newview/scripts/lua/qtest.lua index 009446d0c3..9526f58b04 100644 --- a/indra/newview/scripts/lua/qtest.lua +++ b/indra/newview/scripts/lua/qtest.lua @@ -21,8 +21,8 @@ function resume(co, ...) end -- ------------------ Queue variables are instance-specific ------------------ -q1 = Queue:new() -q2 = Queue:new() +q1 = Queue() +q2 = Queue() q1:Enqueue(17) @@ -33,8 +33,8 @@ assert(q1:Dequeue() == nil) assert(q2:Dequeue() == nil) -- ----------------------------- test WaitQueue ------------------------------ -q1 = WaitQueue:new() -q2 = WaitQueue:new() +q1 = WaitQueue() +q2 = WaitQueue() result = {} values = { 1, 1, 2, 3, 5, 8, 13, 21 } @@ -76,7 +76,7 @@ print('result:', inspect(result)) assert(util.equal(values, result)) -- try incrementally enqueueing values -q3 = WaitQueue:new() +q3 = WaitQueue() result = {} values = { 'This', 'is', 'a', 'test', 'script' } @@ -124,7 +124,7 @@ print(string.format('%q', table.concat(result, ' '))) assert(util.equal(values, result)) -- ----------------------------- test ErrorQueue ----------------------------- -q4 = ErrorQueue:new() +q4 = ErrorQueue() result = {} values = { 'This', 'is', 'a', 'test', 'script' } diff --git a/indra/newview/scripts/lua/test_luafloater_demo.lua b/indra/newview/scripts/lua/test_luafloater_demo.lua index ab638dcdd1..65a31670c8 100644 --- a/indra/newview/scripts/lua/test_luafloater_demo.lua +++ b/indra/newview/scripts/lua/test_luafloater_demo.lua @@ -60,7 +60,7 @@ local resp = leap.request("LLFloaterReg", key) COMMAND_PUMP_NAME = resp.command_name reqid = resp.reqid -catch_events = leap.WaitFor:new(-1, "all_events") +catch_events = leap.WaitFor(-1, "all_events") function catch_events:filter(pump, data) if data.reqid == reqid then return data diff --git a/indra/newview/scripts/lua/test_luafloater_demo2.lua b/indra/newview/scripts/lua/test_luafloater_demo2.lua index 9e24237d28..3903d01e65 100644 --- a/indra/newview/scripts/lua/test_luafloater_demo2.lua +++ b/indra/newview/scripts/lua/test_luafloater_demo2.lua @@ -2,7 +2,7 @@ local Floater = require 'Floater' local leap = require 'leap' local startup = require 'startup' -local flt = Floater:new( +local flt = Floater( 'luafloater_demo.xml', {show_time_lbl = {"right_mouse_down", "double_click"}}) diff --git a/indra/newview/scripts/lua/test_luafloater_gesture_list.lua b/indra/newview/scripts/lua/test_luafloater_gesture_list.lua index 3d9a9b0ad4..a5fd325430 100644 --- a/indra/newview/scripts/lua/test_luafloater_gesture_list.lua +++ b/indra/newview/scripts/lua/test_luafloater_gesture_list.lua @@ -58,7 +58,7 @@ local key = {xml_path = XML_FILE_PATH, op = "showLuaFloater"} key.extra_events={gesture_list = {_event("double_click")}} handleEvents(leap.request("LLFloaterReg", key)) -catch_events = leap.WaitFor:new(-1, "all_events") +catch_events = leap.WaitFor(-1, "all_events") function catch_events:filter(pump, data) if data.reqid == reqid then return data diff --git a/indra/newview/scripts/lua/test_luafloater_gesture_list2.lua b/indra/newview/scripts/lua/test_luafloater_gesture_list2.lua index d702d09c51..bd397ef2a6 100644 --- a/indra/newview/scripts/lua/test_luafloater_gesture_list2.lua +++ b/indra/newview/scripts/lua/test_luafloater_gesture_list2.lua @@ -2,7 +2,7 @@ local Floater = require 'Floater' local LLGesture = require 'LLGesture' local startup = require 'startup' -local flt = Floater:new( +local flt = Floater( "luafloater_gesture_list.xml", {gesture_list = {"double_click"}}) diff --git a/indra/newview/scripts/lua/test_timers.lua b/indra/newview/scripts/lua/test_timers.lua index ed0de070f7..be5001aa16 100644 --- a/indra/newview/scripts/lua/test_timers.lua +++ b/indra/newview/scripts/lua/test_timers.lua @@ -6,9 +6,9 @@ local timers = require 'timers' -- is true, that timeUntilCall() is (true, close to 10), that cancel() returns -- true. After that, isRunning() is false, timeUntilCall() returns (false, 0), -- and a second cancel() returns false. -print('t0:new(10)') +print('t0(10)') start = os.clock() -t0 = timers.Timer:new(10, function() print('t0 fired at', os.clock() - start) end) +t0 = timers.Timer(10, function() print('t0 fired at', os.clock() - start) end) print('t0:isRunning(): ', t0:isRunning()) print('t0:timeUntilCall(): ', t0:timeUntilCall()) print('t0:cancel(): ', t0:cancel()) @@ -18,16 +18,16 @@ print('t0:cancel(): ', t0:cancel()) -- t1 is supposed to fire after 5 seconds, but it doesn't wait, so you see the -- t2 messages immediately after. -print('t1:new(5)') +print('t1(5)') start = os.clock() -t1 = timers.Timer:new(5, function() print('t1 fired at', os.clock() - start) end) +t1 = timers.Timer(5, function() print('t1 fired at', os.clock() - start) end) -- t2 illustrates that instead of passing a callback to new(), you can -- override the timer instance's tick() method. But t2 doesn't wait either, so -- you see the Timer(5) message immediately. -print('t2:new(2)') +print('t2(2)') start = os.clock() -t2 = timers.Timer:new(2) +t2 = timers.Timer(2) function t2:tick() print('t2 fired at', os.clock() - start) end @@ -37,7 +37,7 @@ end -- then the t1 callback message before the Timer(5) completion message. print('Timer(5) waiting') start = os.clock() -timers.Timer:new(5, 'wait') +timers.Timer(5, 'wait') print(string.format('Timer(5) waited %f seconds', os.clock() - start)) -- This test demonstrates a repeating timer. It also shows that you can (but @@ -50,7 +50,7 @@ print(string.format('Timer(5) waited %f seconds', os.clock() - start)) -- it's worth knowing that a coroutine timer callback can be used to manage -- more complex control flows. start = os.clock() -timers.Timer:new( +timers.Timer( 2, coroutine.wrap(function() for i = 1,5 do diff --git a/indra/newview/scripts/lua/timers.lua b/indra/newview/scripts/lua/timers.lua index e0d27a680d..e4938078dc 100644 --- a/indra/newview/scripts/lua/timers.lua +++ b/indra/newview/scripts/lua/timers.lua @@ -1,6 +1,7 @@ -- Access to the viewer's time-delay facilities local leap = require 'leap' +local util = require 'util' local timers = {} @@ -78,6 +79,8 @@ function timers.Timer:new(delay, callback, iterate) return obj end +util.classctor(timers.Timer) + function timers.Timer:tick() error('Pass a callback to Timer:new(), or override Timer:tick()') end diff --git a/indra/newview/scripts/lua/util.lua b/indra/newview/scripts/lua/util.lua index a2191288f6..bfbfc8637c 100644 --- a/indra/newview/scripts/lua/util.lua +++ b/indra/newview/scripts/lua/util.lua @@ -2,6 +2,31 @@ local util = {} +-- Allow MyClass(ctor args...) equivalent to MyClass:new(ctor args...) +-- Usage: +-- local MyClass = {} +-- function MyClass:new(...) +-- ... +-- end +-- ... +-- util.classctor(MyClass) +-- or if your constructor is named something other than MyClass:new(), e.g. +-- MyClass:construct(): +-- util.classctor(MyClass, MyClass.construct) +-- return MyClass +function util.classctor(class, ctor) + -- get the metatable for the passed class + local mt = getmetatable(class) + if mt == nil then + -- if it doesn't already have a metatable, then create one + mt = {} + setmetatable(class, mt) + end + -- now that class has a metatable, set its __call method to the specified + -- constructor method (class.new if not specified) + mt.__call = ctor or class.new +end + -- check if array-like table contains certain value function util.contains(t, v) return table.find(t, v) ~= nil -- cgit v1.2.3 From f2020bff30808d28aec06cce5fed61717fcde7fc Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 11 Jun 2024 21:50:35 -0400 Subject: Fix a couple bugs in startup.lua. The 'startup' table, the module's namespace, must be defined near the top because its local waitfor:process() override references startup. The byname table's metatable's __index() function wants to raise an error if you try to access an undefined entry, but it referenced t[k] to check that, producing infinite recursion. Use rawget(t, k) instead. Also use new leap.WaitFor(args) syntax instead of leap.WaitFor:new(args). --- indra/newview/scripts/lua/startup.lua | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/indra/newview/scripts/lua/startup.lua b/indra/newview/scripts/lua/startup.lua index 4311bb9a60..c3040f94b8 100644 --- a/indra/newview/scripts/lua/startup.lua +++ b/indra/newview/scripts/lua/startup.lua @@ -12,6 +12,8 @@ local function dbg(...) end -- local dbg = require 'printf' -- --------------------------------------------------------------------------- +local startup = {} + -- Get the list of startup states from the viewer. local bynum = leap.request('LLStartUp', {op='getStateTable'})['table'] @@ -19,7 +21,7 @@ local byname = setmetatable( {}, -- set metatable to throw an error if you look up invalid state name {__index=function(t, k) - local v = t[k] + local v = rawget(t, k) if v then return v end @@ -35,7 +37,7 @@ end -- specialize a WaitFor to track the viewer's startup state local startup_pump = 'StartupState' -local waitfor = leap.WaitFor:new(0, startup_pump) +local waitfor = leap.WaitFor(0, startup_pump) function waitfor:filter(pump, data) if pump == self.name then return data @@ -57,8 +59,6 @@ leap.request(leap.cmdpump(), leap.send('LLStartUp', {op='postStartupState'}) -- --------------------------------------------------------------------------- -startup = {} - -- wait for response from postStartupState while not startup._state do dbg('startup.state() waiting for first StartupState event') @@ -98,4 +98,3 @@ function startup.wait(state) end return startup - -- cgit v1.2.3 From 44182d0719c209aabe0d80aea291a9e3e45b1e59 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 11 Jun 2024 21:59:31 -0400 Subject: Add to UI.lua a set of 'LLWindow' listener operations. Add listviews(), viewinfo(), click(), doubleclick(), drag(), keypress() and type(). WIP: These are ported from Python LEAP equivalents, but the Lua implementation has only been partially tested. --- indra/newview/scripts/lua/UI.lua | 113 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 2 deletions(-) diff --git a/indra/newview/scripts/lua/UI.lua b/indra/newview/scripts/lua/UI.lua index 24f822bbd9..eb1a4017c7 100644 --- a/indra/newview/scripts/lua/UI.lua +++ b/indra/newview/scripts/lua/UI.lua @@ -1,9 +1,14 @@ --- Engage the UI LLEventAPI +-- Engage the viewer's UI -leap = require 'leap' +local leap = require 'leap' +local Timer = (require 'timers').Timer +local mapargs = require 'mapargs' local UI = {} +-- *************************************************************************** +-- registered menu actions +-- *************************************************************************** function UI.call(func, parameter) -- 'call' is fire-and-forget leap.request('UI', {op='call', ['function']=func, parameter=parameter}) @@ -13,4 +18,108 @@ function UI.getValue(path) return leap.request('UI', {op='getValue', path=path})['value'] end +-- *************************************************************************** +-- UI views +-- *************************************************************************** +-- Either: +-- wreq{op='Something', a=1, b=2, ...} +-- or: +-- (args should be local, as this wreq() call modifies it) +-- local args = {a=1, b=2, ...} +-- wreq('Something', args) +local function wreq(op_or_data, data_if_op) + if data_if_op ~= nil then + -- this is the wreq(op, data) form + data_if_op.op = op_or_data + op_or_data = data_if_op + end + return leap.request('LLWindow', op_or_data) +end + +-- omit 'parent' to list all view paths +function UI.listviews(parent) + return wreq{op='getPaths', under=parent} +end + +function UI.viewinfo(path) + return wreq{op='getInfo', path=path} +end + +-- *************************************************************************** +-- mouse actions +-- *************************************************************************** +-- pass a table: +-- UI.click{path=path +-- [, button='LEFT' | 'CENTER' | 'RIGHT'] +-- [, x=x, y=y] +-- [, hold=duration]} +function UI.click(...) + local args = mapargs('path,button,x,y,hold', ...) + args.button = args.button or 'LEFT' + local hold = args.hold or 1.0 + wreq('mouseMove', args) + wreq('mouseDown', args) + Timer(hold, 'wait') + wreq('mouseUp', args) +end + +-- pass a table as for UI.click() +function UI.doubleclick(...) + local args = mapargs('path,button,x,y', ...) + args.button = args.button or 'LEFT' + wreq('mouseDown', args) + wreq('mouseUp', args) + wreq('mouseDown', args) + wreq('mouseUp', args) +end + +-- UI.drag{path=, xoff=, yoff=} +function UI.drag(...) + local args = mapargs('path,xoff,yoff', ...) + -- query the specified path + local rect = UI.viewinfo(args.path).rect + local centerx = math.floor(rect.left + (rect.right - rect.left)/2) + local centery = math.floor(rect.bottom + (rect.top - rect.bottom)/2) + wreq{op='mouseMove', path=args.path, x=centerx, y=centery} + wreq{op='mouseDown', path=args.path, button='LEFT'} + wreq{op='mouseMove', path=args.path, x=centerx + args.xoff, y=centery + args.yoff} + wreq{op='mouseUp', path=args.path, button='LEFT'} +end + +-- *************************************************************************** +-- keyboard actions +-- *************************************************************************** +-- pass a table: +-- UI.keypress{ +-- [path=path] -- if omitted, default input field +-- [, char='x'] -- requires one of char, keycode, keysym +-- [, keycode=120] +-- keysym per https://github.com/secondlife/viewer/blob/main/indra/llwindow/llkeyboard.cpp#L68-L124 +-- [, keysym='Enter'] +-- [, mask={'SHIFT', 'CTL', 'ALT', 'MAC_CONTROL'}] -- some subset of these +-- } +function UI.keypress(...) + local args = mapargs('path,char,keycode,keysym,mask', ...) + if args.char == '\n' then + args.char = nil + args.keysym = 'Enter' + end + return wreq('keyDown', args) +end + +-- UI.type{text=, path=} +function UI.type(...) + local args = mapargs('text,path', ...) + if #args.text > 0 then + -- The caller's path may be specified in a way that requires recursively + -- searching parts of the LLView tree. No point in doing that more than + -- once. Capture the actual path found by that first call and use that for + -- subsequent calls. + local path = UI.keypress{path=args.path, char=string.sub(args.text, 1, 1)}.path + for i = 2, #args.text do + UI.keypress{path=path, char=string.sub(args.text, i, i)} + end + end +end + return UI -- cgit v1.2.3 From beb28c4351f3ef622c45f3603df0ba9c5e162793 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 11 Jun 2024 22:01:40 -0400 Subject: Add login.lua module with login() function. The nullary login() call (login with saved credentials) has been tested, but the binary login(username, password) call is known not to work yet. --- indra/newview/scripts/lua/login.lua | 19 +++++++++++++++++++ indra/newview/scripts/lua/test_login.lua | 7 +++++++ 2 files changed, 26 insertions(+) create mode 100644 indra/newview/scripts/lua/login.lua create mode 100644 indra/newview/scripts/lua/test_login.lua diff --git a/indra/newview/scripts/lua/login.lua b/indra/newview/scripts/lua/login.lua new file mode 100644 index 0000000000..0d8591cace --- /dev/null +++ b/indra/newview/scripts/lua/login.lua @@ -0,0 +1,19 @@ +local UI = require 'UI' +local leap = require 'leap' + +local function login(username, password) + if username and password then + local userpath = '//username_combo/Combo Text Entry' + local passpath = '//password_edit' + -- first clear anything presently in those text fields + for _, path in pairs({userpath, passpath}) do + UI.click(path) + UI.keypress{keysym='Backsp', path=path} + end + UI.type{path=userpath, text=username} + UI.type{path=passpath, text=password} + end + leap.send('LLPanelLogin', {op='onClickConnect'}) +end + +return login diff --git a/indra/newview/scripts/lua/test_login.lua b/indra/newview/scripts/lua/test_login.lua new file mode 100644 index 0000000000..6df52b08c2 --- /dev/null +++ b/indra/newview/scripts/lua/test_login.lua @@ -0,0 +1,7 @@ +startup = require 'startup' +login = require 'login' + +startup.wait('STATE_LOGIN_WAIT') +login() +-- WIP: not working as of 2024-06-11 +-- login('My Username', 'password') -- cgit v1.2.3 From f95dc89d5e7481f4e02953617ce7a13feb87d27a Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 11 Jun 2024 22:05:00 -0400 Subject: Add popup.lua, a preliminary API for viewer notifications. WIP: This is known not to work yet. --- indra/newview/scripts/lua/popup.lua | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 indra/newview/scripts/lua/popup.lua diff --git a/indra/newview/scripts/lua/popup.lua b/indra/newview/scripts/lua/popup.lua new file mode 100644 index 0000000000..65b04b513e --- /dev/null +++ b/indra/newview/scripts/lua/popup.lua @@ -0,0 +1,32 @@ +leap = require 'leap' + +-- notification is any name defined in notifications.xml as +-- +-- vars is a table providing values for [VAR] substitution keys in the +-- notification body. +local popup_meta = { + -- setting this function as getmetatable(popup).__call() means this gets + -- called when a consumer calls popup(notification, vars, payload) + __call = function(self, notification, vars, payload) + return leap.request('LLNotifications', + {op='requestAdd', name=notification, + substitutions=vars, + payload=payload or {}}) + end +} + +local popup = setmetatable({}, popup_meta) + +function popup:alert(message) + return self('GenericAlert', {MESSAGE=message}) +end + +function popup:alertOK(message) + return self('GenericAlertOK', {MESSAGE=message}) +end + +function popup:alertYesCancel(message) + return self('GenericAlertYesCancel', {MESSAGE=message}) +end + +return popup -- cgit v1.2.3 From ffdcf33364ebfdc1829cb7c7eea6ae4c908d12f1 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 12 Jun 2024 16:42:39 -0400 Subject: Extract TempSet from llcallbacklist.cpp into its own tempset.h. --- indra/llcommon/CMakeLists.txt | 1 + indra/llcommon/llcallbacklist.cpp | 27 +------------------------- indra/llcommon/tempset.h | 41 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 26 deletions(-) create mode 100755 indra/llcommon/tempset.h diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index d5440d6bc8..20670d7ebe 100644 --- a/indra/llcommon/CMakeLists.txt +++ b/indra/llcommon/CMakeLists.txt @@ -259,6 +259,7 @@ set(llcommon_HEADER_FILES lualistener.h stdtypes.h stringize.h + tempset.h threadpool.h threadpool_fwd.h threadsafeschedule.h diff --git a/indra/llcommon/llcallbacklist.cpp b/indra/llcommon/llcallbacklist.cpp index 015475a903..555c793333 100644 --- a/indra/llcommon/llcallbacklist.cpp +++ b/indra/llcommon/llcallbacklist.cpp @@ -29,6 +29,7 @@ #include "llerror.h" #include "llexception.h" #include "llsdutil.h" +#include "tempset.h" #include #include #include @@ -292,32 +293,6 @@ void Timers::setTimeslice(F32 timeslice) } } -// RAII class to set specified variable to specified value -// only for the duration of containing scope -template -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; - VALUE mOldValue; -}; - bool Timers::tick() { // Fetch current time only on entry, even though running some mQueue task diff --git a/indra/llcommon/tempset.h b/indra/llcommon/tempset.h new file mode 100755 index 0000000000..e1496bd5fc --- /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 +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; + VALUE mOldValue; +}; + +#endif /* ! defined(LL_TEMPSET_H) */ -- cgit v1.2.3 From 241156cf9831e30a3bbb529478e73aaf233a759b Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 12 Jun 2024 16:45:35 -0400 Subject: Make popup() directly pass payload. The expression (payload or {}) is unnecessary, since that value will be converted to LLSD -- and both Lua nil and empty table convert to LLSD::isUndefined(). --- indra/newview/scripts/lua/popup.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indra/newview/scripts/lua/popup.lua b/indra/newview/scripts/lua/popup.lua index 65b04b513e..9eecc46753 100644 --- a/indra/newview/scripts/lua/popup.lua +++ b/indra/newview/scripts/lua/popup.lua @@ -11,7 +11,7 @@ local popup_meta = { return leap.request('LLNotifications', {op='requestAdd', name=notification, substitutions=vars, - payload=payload or {}}) + payload=payload}) end } -- cgit v1.2.3 From 52ff9dfe404e4a49ac43185004218ce4e50e2971 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 12 Jun 2024 16:49:19 -0400 Subject: For a single string concatenation, use operator+(). stringize() constructs, populates and destroys a std::ostringstream, which is actually less efficient than directly allocating a std::string big enough for the result of operator+(). Maybe someday we'll specialize stringize(p0, p1) for the case in which they're both string-like, and invoke operator+() for that situation... --- indra/newview/llluamanager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indra/newview/llluamanager.cpp b/indra/newview/llluamanager.cpp index 97779a12ad..6f72eb4142 100644 --- a/indra/newview/llluamanager.cpp +++ b/indra/newview/llluamanager.cpp @@ -254,7 +254,7 @@ void LLLUAmanager::runScriptLine(LuaState& L, const std::string& chunk, script_r if (shortchunk.length() > shortlen) shortchunk = stringize(shortchunk.substr(0, shortlen), "..."); - std::string desc{ stringize("lua: ", shortchunk) }; + std::string desc{ "lua: " + shortchunk }; LLCoros::instance().launch(desc, [&L, desc, chunk, cb]() { auto [count, result] = L.expr(desc, chunk); -- cgit v1.2.3 From 128514da9e1b24e8d817ec90b53dea9506f31101 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 12 Jun 2024 16:51:33 -0400 Subject: LuaState::expr() has log messages for ending, add for starting. It's helpful to see when expr() is actually going to start running a particular Lua chunk. We already report not only when it's done, but also if/when we start and finish a p.s. fiber.run() call. --- indra/llcommon/lua_function.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 08bc65e0c5..7c80f65ff9 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -529,6 +529,7 @@ std::pair LuaState::expr(const std::string& desc, const std::string& lluau::check_interrupts_counter(L); }; + LL_INFOS("Lua") << desc << " run" << LL_ENDL; if (! checkLua(desc, lluau::dostring(mState, desc, text))) { LL_WARNS("Lua") << desc << " error: " << mError << LL_ENDL; -- cgit v1.2.3 From 2ad31a8316c56edfcd78ce494edfcd98209a2cfa Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 12 Jun 2024 16:59:05 -0400 Subject: Defend LLFloaterLUADebug against recursive calls to handlers. The special case of a Lua snippet that indirectly invokes the "LLNotifications" listener can result in a recursive call to LLFloaterLUADebug's handler methods. Defend against that case. --- indra/newview/llfloaterluadebug.cpp | 24 ++++++++++++++++++++++++ indra/newview/llfloaterluadebug.h | 1 + 2 files changed, 25 insertions(+) diff --git a/indra/newview/llfloaterluadebug.cpp b/indra/newview/llfloaterluadebug.cpp index 60571d6247..6da28aef5e 100644 --- a/indra/newview/llfloaterluadebug.cpp +++ b/indra/newview/llfloaterluadebug.cpp @@ -41,6 +41,7 @@ #include "llsdutil.h" #include "lua_function.h" #include "stringize.h" +#include "tempset.h" LLFloaterLUADebug::LLFloaterLUADebug(const LLSD &key) @@ -77,6 +78,17 @@ LLFloaterLUADebug::~LLFloaterLUADebug() void LLFloaterLUADebug::onExecuteClicked() { + // Empirically, running Lua code that indirectly invokes the + // "LLNotifications" listener can result (via mysterious labyrinthine + // viewer UI byways) in a recursive call to this handler. We've seen Bad + // Things happen to the viewer with a second call to runScriptLine() with + // the same cmd on the same LuaState. + if (mExecuting) + { + LL_DEBUGS("Lua") << "recursive call to onExecuteClicked()" << LL_ENDL; + return; + } + TempSet executing(mExecuting, true); mResultOutput->setValue(""); std::string cmd = mLineInput->getText(); @@ -94,6 +106,12 @@ void LLFloaterLUADebug::onBtnBrowse() void LLFloaterLUADebug::onBtnRun() { + if (mExecuting) + { + LL_DEBUGS("Lua") << "recursive call to onBtnRun()" << LL_ENDL; + return; + } + TempSet executing(mExecuting, true); std::vector filenames; std::string filepath = mScriptPath->getText(); if (!filepath.empty()) @@ -105,6 +123,12 @@ void LLFloaterLUADebug::onBtnRun() void LLFloaterLUADebug::runSelectedScript(const std::vector &filenames) { + if (mExecuting) + { + LL_DEBUGS("Lua") << "recursive call to runSelectedScript()" << LL_ENDL; + return; + } + TempSet executing(mExecuting, true); mResultOutput->setValue(""); std::string filepath = filenames[0]; diff --git a/indra/newview/llfloaterluadebug.h b/indra/newview/llfloaterluadebug.h index 7418174570..4d9e2fabca 100644 --- a/indra/newview/llfloaterluadebug.h +++ b/indra/newview/llfloaterluadebug.h @@ -66,6 +66,7 @@ private: LLLineEditor* mLineInput; LLLineEditor* mScriptPath; LuaState mState; + bool mExecuting{ false }; }; #endif // LL_LLFLOATERLUADEBUG_H -- cgit v1.2.3 From 6f50aa0e87804ceebce93e29dba4ce073c70cb62 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 12 Jun 2024 17:22:18 -0400 Subject: Provide LUA Debug Console feedback for user typing LUA string. When the user explicitly types 'return expression[, expression]...' we convert the result of the expressions to LLSD and format them into the LUA Debug Console, which serves as a useful acknowledgment. But until now, if the user neither invoked print() nor ran a 'return' statement, the LUA Debug Console output remained empty. This could be a little disconcerting: you click Execute, or press Enter, and apparently nothing happens. You must either monitor viewer log output, or simply trust that the Lua snippet ran. When there are no 'return' results, at least emit 'ok'. But when the user is entering a series of no-output commands, vary the 'ok' output by appending a counter: 'ok 1', 'ok 2' etc. --- indra/newview/llfloaterluadebug.cpp | 8 +++++++- indra/newview/llfloaterluadebug.h | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/indra/newview/llfloaterluadebug.cpp b/indra/newview/llfloaterluadebug.cpp index 6da28aef5e..f715327ec8 100644 --- a/indra/newview/llfloaterluadebug.cpp +++ b/indra/newview/llfloaterluadebug.cpp @@ -153,13 +153,19 @@ void LLFloaterLUADebug::completion(int count, const LLSD& result) mResultOutput->endOfDoc(); return; } + if (count == 0) + { + // no results + mResultOutput->pasteTextWithLinebreaks(stringize("ok ", ++mAck)); + return; + } if (count == 1) { // single result mResultOutput->pasteTextWithLinebreaks(stringize(result)); return; } - // 0 or multiple results + // multiple results const char* sep = ""; for (const auto& item : llsd::inArray(result)) { diff --git a/indra/newview/llfloaterluadebug.h b/indra/newview/llfloaterluadebug.h index 4d9e2fabca..ae30b7cf25 100644 --- a/indra/newview/llfloaterluadebug.h +++ b/indra/newview/llfloaterluadebug.h @@ -66,6 +66,7 @@ private: LLLineEditor* mLineInput; LLLineEditor* mScriptPath; LuaState mState; + U32 mAck{ 0 }; bool mExecuting{ false }; }; -- cgit v1.2.3 From d3b4b77a95baf66dcdb90c4312332bc2ac2c7663 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 12 Jun 2024 17:32:18 -0400 Subject: Add LL_DEBUGS("LLCoros") start/end messages. We have log messages when a coroutine terminates abnormally, but we don't report either when it starts or when it terminates normally. Address that. --- indra/llcommon/llcoros.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/indra/llcommon/llcoros.cpp b/indra/llcommon/llcoros.cpp index a6d7988256..c28baa5747 100644 --- a/indra/llcommon/llcoros.cpp +++ b/indra/llcommon/llcoros.cpp @@ -270,7 +270,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 @@ -355,10 +355,12 @@ void LLCoros::toplevel(std::string name, callable_t callable) // set it as current mCurrent.reset(&corodata); + LL_DEBUGS("LLCoros") << "entering " << name << LL_ENDL; // run the code the caller actually wants in the coroutine try { sehandle(callable); + LL_DEBUGS("LLCoros") << "done " << name << LL_ENDL; } catch (const Stop& exc) { @@ -370,7 +372,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 (...) { -- cgit v1.2.3 From 7603ba216f35b44327c7fe9ed0a77d69356e0395 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 12 Jun 2024 17:32:51 -0400 Subject: Avoid messing up Lua's global namespace in 'require' modules. --- indra/newview/scripts/lua/LLChat.lua | 2 +- indra/newview/scripts/lua/LLDebugSettings.lua | 2 +- indra/newview/scripts/lua/LLFloaterAbout.lua | 2 +- indra/newview/scripts/lua/LLGesture.lua | 2 +- indra/newview/scripts/lua/popup.lua | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/indra/newview/scripts/lua/LLChat.lua b/indra/newview/scripts/lua/LLChat.lua index 7db538e837..78dca765e8 100644 --- a/indra/newview/scripts/lua/LLChat.lua +++ b/indra/newview/scripts/lua/LLChat.lua @@ -1,4 +1,4 @@ -leap = require 'leap' +local leap = require 'leap' local LLChat = {} diff --git a/indra/newview/scripts/lua/LLDebugSettings.lua b/indra/newview/scripts/lua/LLDebugSettings.lua index c250019a00..cff1a63c21 100644 --- a/indra/newview/scripts/lua/LLDebugSettings.lua +++ b/indra/newview/scripts/lua/LLDebugSettings.lua @@ -1,4 +1,4 @@ -leap = require 'leap' +local leap = require 'leap' local LLDebugSettings = {} diff --git a/indra/newview/scripts/lua/LLFloaterAbout.lua b/indra/newview/scripts/lua/LLFloaterAbout.lua index 44afee2e5c..a6e42d364f 100644 --- a/indra/newview/scripts/lua/LLFloaterAbout.lua +++ b/indra/newview/scripts/lua/LLFloaterAbout.lua @@ -1,6 +1,6 @@ -- Engage the LLFloaterAbout LLEventAPI -leap = require 'leap' +local leap = require 'leap' local LLFloaterAbout = {} diff --git a/indra/newview/scripts/lua/LLGesture.lua b/indra/newview/scripts/lua/LLGesture.lua index cb410446d7..343b611e2c 100644 --- a/indra/newview/scripts/lua/LLGesture.lua +++ b/indra/newview/scripts/lua/LLGesture.lua @@ -1,6 +1,6 @@ -- Engage the LLGesture LLEventAPI -leap = require 'leap' +local leap = require 'leap' local LLGesture = {} diff --git a/indra/newview/scripts/lua/popup.lua b/indra/newview/scripts/lua/popup.lua index 9eecc46753..8a01ab7836 100644 --- a/indra/newview/scripts/lua/popup.lua +++ b/indra/newview/scripts/lua/popup.lua @@ -1,4 +1,4 @@ -leap = require 'leap' +local leap = require 'leap' -- notification is any name defined in notifications.xml as -- -- cgit v1.2.3 From dc91db0b85c5db1c20dccebd64c98419e371a29f Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Fri, 14 Jun 2024 11:28:22 -0400 Subject: Fix a minor but nagging Python build-time warning: invalid regexp. --- scripts/packages-formatter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/packages-formatter.py b/scripts/packages-formatter.py index 4449111e46..5d31702e76 100755 --- a/scripts/packages-formatter.py +++ b/scripts/packages-formatter.py @@ -42,7 +42,7 @@ _autobuild_env=os.environ.copy() # Coerce stdout encoding to utf-8 as cygwin's will be detected as cp1252 otherwise. _autobuild_env["PYTHONIOENCODING"] = "utf-8" -pkg_line=re.compile('^([\w-]+):\s+(.*)$') +pkg_line=re.compile(r'^([\w-]+):\s+(.*)$') def autobuild(*args): """ -- cgit v1.2.3 From f7137765438f149cbae6f3b18da45dce75a25336 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Fri, 14 Jun 2024 12:16:12 -0400 Subject: Move Lua modules for 'require' to indra/newview/scripts/lua/require. Make viewer_manifest.py copy them into the viewer install image. Make the require() function look for them there. --- indra/newview/llluamanager.cpp | 4 +- indra/newview/scripts/lua/ErrorQueue.lua | 37 -- indra/newview/scripts/lua/Floater.lua | 146 ------ indra/newview/scripts/lua/LLChat.lua | 17 - indra/newview/scripts/lua/LLDebugSettings.lua | 17 - indra/newview/scripts/lua/LLFloaterAbout.lua | 11 - indra/newview/scripts/lua/LLGesture.lua | 23 - indra/newview/scripts/lua/Queue.lua | 51 -- indra/newview/scripts/lua/UI.lua | 125 ----- indra/newview/scripts/lua/WaitQueue.lua | 88 ---- indra/newview/scripts/lua/coro.lua | 67 --- indra/newview/scripts/lua/fiber.lua | 340 ------------- indra/newview/scripts/lua/inspect.lua | 371 -------------- indra/newview/scripts/lua/leap.lua | 550 --------------------- indra/newview/scripts/lua/printf.lua | 19 - indra/newview/scripts/lua/require/ErrorQueue.lua | 37 ++ indra/newview/scripts/lua/require/Floater.lua | 146 ++++++ indra/newview/scripts/lua/require/LLChat.lua | 17 + .../scripts/lua/require/LLDebugSettings.lua | 17 + .../newview/scripts/lua/require/LLFloaterAbout.lua | 11 + indra/newview/scripts/lua/require/LLGesture.lua | 23 + indra/newview/scripts/lua/require/Queue.lua | 51 ++ indra/newview/scripts/lua/require/UI.lua | 125 +++++ indra/newview/scripts/lua/require/WaitQueue.lua | 88 ++++ indra/newview/scripts/lua/require/coro.lua | 67 +++ indra/newview/scripts/lua/require/fiber.lua | 340 +++++++++++++ indra/newview/scripts/lua/require/inspect.lua | 371 ++++++++++++++ indra/newview/scripts/lua/require/leap.lua | 550 +++++++++++++++++++++ indra/newview/scripts/lua/require/printf.lua | 19 + indra/newview/scripts/lua/require/startup.lua | 100 ++++ indra/newview/scripts/lua/require/timers.lua | 104 ++++ indra/newview/scripts/lua/require/util.lua | 69 +++ indra/newview/scripts/lua/startup.lua | 100 ---- indra/newview/scripts/lua/timers.lua | 104 ---- indra/newview/scripts/lua/util.lua | 69 --- indra/newview/viewer_manifest.py | 4 +- 36 files changed, 2140 insertions(+), 2138 deletions(-) delete mode 100644 indra/newview/scripts/lua/ErrorQueue.lua delete mode 100644 indra/newview/scripts/lua/Floater.lua delete mode 100644 indra/newview/scripts/lua/LLChat.lua delete mode 100644 indra/newview/scripts/lua/LLDebugSettings.lua delete mode 100644 indra/newview/scripts/lua/LLFloaterAbout.lua delete mode 100644 indra/newview/scripts/lua/LLGesture.lua delete mode 100644 indra/newview/scripts/lua/Queue.lua delete mode 100644 indra/newview/scripts/lua/UI.lua delete mode 100644 indra/newview/scripts/lua/WaitQueue.lua delete mode 100644 indra/newview/scripts/lua/coro.lua delete mode 100644 indra/newview/scripts/lua/fiber.lua delete mode 100644 indra/newview/scripts/lua/inspect.lua delete mode 100644 indra/newview/scripts/lua/leap.lua delete mode 100644 indra/newview/scripts/lua/printf.lua create mode 100644 indra/newview/scripts/lua/require/ErrorQueue.lua create mode 100644 indra/newview/scripts/lua/require/Floater.lua create mode 100644 indra/newview/scripts/lua/require/LLChat.lua create mode 100644 indra/newview/scripts/lua/require/LLDebugSettings.lua create mode 100644 indra/newview/scripts/lua/require/LLFloaterAbout.lua create mode 100644 indra/newview/scripts/lua/require/LLGesture.lua create mode 100644 indra/newview/scripts/lua/require/Queue.lua create mode 100644 indra/newview/scripts/lua/require/UI.lua create mode 100644 indra/newview/scripts/lua/require/WaitQueue.lua create mode 100644 indra/newview/scripts/lua/require/coro.lua create mode 100644 indra/newview/scripts/lua/require/fiber.lua create mode 100644 indra/newview/scripts/lua/require/inspect.lua create mode 100644 indra/newview/scripts/lua/require/leap.lua create mode 100644 indra/newview/scripts/lua/require/printf.lua create mode 100644 indra/newview/scripts/lua/require/startup.lua create mode 100644 indra/newview/scripts/lua/require/timers.lua create mode 100644 indra/newview/scripts/lua/require/util.lua delete mode 100644 indra/newview/scripts/lua/startup.lua delete mode 100644 indra/newview/scripts/lua/timers.lua delete mode 100644 indra/newview/scripts/lua/util.lua diff --git a/indra/newview/llluamanager.cpp b/indra/newview/llluamanager.cpp index 6f72eb4142..5aa47d7566 100644 --- a/indra/newview/llluamanager.cpp +++ b/indra/newview/llluamanager.cpp @@ -386,10 +386,10 @@ void LLRequireResolver::findModule() std::vector lib_paths { - gDirUtilp->getExpandedFilename(LL_PATH_SCRIPTS, "lua"), + gDirUtilp->getExpandedFilename(LL_PATH_SCRIPTS, "lua", "require"), #ifdef LL_TEST // Build-time tests don't have the app bundle - use source tree. - fsyspath(__FILE__).parent_path() / "scripts" / "lua", + fsyspath(__FILE__).parent_path() / "scripts" / "lua" / "require", #endif }; diff --git a/indra/newview/scripts/lua/ErrorQueue.lua b/indra/newview/scripts/lua/ErrorQueue.lua deleted file mode 100644 index e6e9a5ef48..0000000000 --- a/indra/newview/scripts/lua/ErrorQueue.lua +++ /dev/null @@ -1,37 +0,0 @@ --- 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 function dbg(...) end --- local dbg = require('printf') -local util = require('util') - -local ErrorQueue = WaitQueue() - -util.classctor(ErrorQueue) - -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. - dbg('Setting self._closed to %q', message) - self._closed = message - self:_wake_waiters() -end - -function ErrorQueue:Dequeue() - local value = WaitQueue.Dequeue(self) - dbg('ErrorQueue:Dequeue: base Dequeue() got %s', value) - 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/Floater.lua b/indra/newview/scripts/lua/Floater.lua deleted file mode 100644 index d057a74386..0000000000 --- a/indra/newview/scripts/lua/Floater.lua +++ /dev/null @@ -1,146 +0,0 @@ --- Floater base class - -local leap = require 'leap' -local fiber = require 'fiber' -local util = require 'util' - --- list of all the events that a LLLuaFloater might send -local event_list = leap.request("LLFloaterReg", {op="getFloaterEvents"}).events -local event_set = {} -for _, event in pairs(event_list) do - event_set[event] = true -end - -local function _event(event_name) - if not event_set[event_name] then - error("Incorrect event name: " .. event_name, 3) - end - return event_name -end - --- --------------------------------------------------------------------------- -local Floater = {} - --- Pass: --- relative file path to floater's XUI definition file --- optional: sign up for additional events for defined control --- {={action1, action2, ...}} -function Floater:new(path, extra) - local obj = setmetatable({}, self) - self.__index = self - - local path_parts = string.split(path, '/') - obj.name = 'Floater ' .. path_parts[#path_parts] - - obj._command = {op="showLuaFloater", xml_path=LL.abspath(path)} - if extra then - -- validate each of the actions for each specified control - for control, actions in pairs(extra) do - for _, action in pairs(actions) do - _event(action) - end - end - obj._command.extra_events = extra - end - - return obj -end - -util.classctor(Floater) - -function Floater:show() - -- leap.eventstream() returns the first response, and launches a - -- background fiber to call the passed callback with all subsequent - -- responses. - local event = leap.eventstream( - 'LLFloaterReg', - self._command, - -- handleEvents() returns false when done. - -- eventstream() expects a true return when done. - function(event) return not self:handleEvents(event) end) - self._pump = event.command_name - -- we might need the returned reqid to cancel the eventstream() fiber - self.reqid = event.reqid - - -- The response to 'showLuaFloater' *is* the 'post_build' event. Check if - -- subclass has a post_build() method. Honor the convention that if - -- handleEvents() returns false, we're done. - if not self:handleEvents(event) then - return - end -end - -function Floater:post(action) - leap.send(self._pump, action) -end - -function Floater:request(action) - return leap.request(self._pump, action) -end - --- local inspect = require 'inspect' - -function Floater:handleEvents(event_data) - local event = event_data.event - if event_set[event] == nil then - LL.print_warning(string.format('%s received unknown event %q', self.name, event)) - end - - -- Before checking for a general (e.g.) commit() method, first look for - -- commit_ctrl_name(): in other words, concatenate the event name with the - -- ctrl_name, with an underscore between. If there exists such a specific - -- method, call that. - local handler, ret - if event_data.ctrl_name then - local specific = event .. '_' .. event_data.ctrl_name - handler = self[specific] - if handler then - ret = handler(self, event_data) - -- Avoid 'return ret or true' because we explicitly want to allow - -- the handler to return false. - if ret ~= nil then - return ret - else - return true - end - end - end - - -- No specific "event_on_ctrl()" method found; try just "event()" - handler = self[event] - if handler then - ret = handler(self, event_data) - if ret ~= nil then - return ret - end --- else --- print(string.format('%s ignoring event %s', self.name, inspect(event_data))) - end - - -- We check for event() method before recognizing floater_close in case - -- the consumer needs to react specially to closing the floater. Now that - -- we've checked, recognize it ourselves. Returning false terminates the - -- anonymous fiber function launched by leap.eventstream(). - if event == _event('floater_close') then - LL.print_warning(self.name .. ' closed') - return false - end - return true -end - --- onCtrl() permits a different dispatch style in which the general event() --- method explicitly calls (e.g.) --- self:onCtrl(event_data, { --- ctrl_name=function() --- self:post(...) --- end, --- ... --- }) -function Floater:onCtrl(event_data, ctrl_map) - local handler = ctrl_map[event_data.ctrl_name] - if handler then - handler() - end -end - -return Floater diff --git a/indra/newview/scripts/lua/LLChat.lua b/indra/newview/scripts/lua/LLChat.lua deleted file mode 100644 index 78dca765e8..0000000000 --- a/indra/newview/scripts/lua/LLChat.lua +++ /dev/null @@ -1,17 +0,0 @@ -local leap = require 'leap' - -local LLChat = {} - -function LLChat.sendNearby(msg) - leap.send('LLChatBar', {op='sendChat', message=msg}) -end - -function LLChat.sendWhisper(msg) - leap.send('LLChatBar', {op='sendChat', type='whisper', message=msg}) -end - -function LLChat.sendShout(msg) - leap.send('LLChatBar', {op='sendChat', type='shout', message=msg}) -end - -return LLChat diff --git a/indra/newview/scripts/lua/LLDebugSettings.lua b/indra/newview/scripts/lua/LLDebugSettings.lua deleted file mode 100644 index cff1a63c21..0000000000 --- a/indra/newview/scripts/lua/LLDebugSettings.lua +++ /dev/null @@ -1,17 +0,0 @@ -local leap = require 'leap' - -local LLDebugSettings = {} - -function LLDebugSettings.set(name, value) - leap.request('LLViewerControl', {op='set', group='Global', key=name, value=value}) -end - -function LLDebugSettings.toggle(name) - leap.request('LLViewerControl', {op='toggle', group='Global', key=name}) -end - -function LLDebugSettings.get(name) - return leap.request('LLViewerControl', {op='get', group='Global', key=name})['value'] -end - -return LLDebugSettings diff --git a/indra/newview/scripts/lua/LLFloaterAbout.lua b/indra/newview/scripts/lua/LLFloaterAbout.lua deleted file mode 100644 index a6e42d364f..0000000000 --- a/indra/newview/scripts/lua/LLFloaterAbout.lua +++ /dev/null @@ -1,11 +0,0 @@ --- Engage the LLFloaterAbout LLEventAPI - -local leap = require 'leap' - -local LLFloaterAbout = {} - -function LLFloaterAbout.getInfo() - return leap.request('LLFloaterAbout', {op='getInfo'}) -end - -return LLFloaterAbout diff --git a/indra/newview/scripts/lua/LLGesture.lua b/indra/newview/scripts/lua/LLGesture.lua deleted file mode 100644 index 343b611e2c..0000000000 --- a/indra/newview/scripts/lua/LLGesture.lua +++ /dev/null @@ -1,23 +0,0 @@ --- Engage the LLGesture LLEventAPI - -local leap = require 'leap' - -local LLGesture = {} - -function LLGesture.getActiveGestures() - return leap.request('LLGesture', {op='getActiveGestures'})['gestures'] -end - -function LLGesture.isGesturePlaying(id) - return leap.request('LLGesture', {op='isGesturePlaying', id=id})['playing'] -end - -function LLGesture.startGesture(id) - leap.send('LLGesture', {op='startGesture', id=id}) -end - -function LLGesture.stopGesture(id) - leap.send('LLGesture', {op='stopGesture', id=id}) -end - -return LLGesture diff --git a/indra/newview/scripts/lua/Queue.lua b/indra/newview/scripts/lua/Queue.lua deleted file mode 100644 index 5bc72e4057..0000000000 --- a/indra/newview/scripts/lua/Queue.lua +++ /dev/null @@ -1,51 +0,0 @@ --- from https://create.roblox.com/docs/luau/queues#implementing-queues, --- amended per https://www.lua.org/pil/16.1.html - --- While coding some scripting in Lua --- I found that I needed a queua --- I thought of linked list --- But had to resist --- For fear it might be too obscua. - -local util = require 'util' - -local Queue = {} - -function Queue:new() - local obj = setmetatable({}, self) - self.__index = self - - obj._first = 0 - obj._last = -1 - obj._queue = {} - - return obj -end - -util.classctor(Queue) - --- Check if the queue is empty -function Queue:IsEmpty() - 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 -end - --- Remove a value from the queue -function Queue:Dequeue() - 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/UI.lua b/indra/newview/scripts/lua/UI.lua deleted file mode 100644 index eb1a4017c7..0000000000 --- a/indra/newview/scripts/lua/UI.lua +++ /dev/null @@ -1,125 +0,0 @@ --- Engage the viewer's UI - -local leap = require 'leap' -local Timer = (require 'timers').Timer -local mapargs = require 'mapargs' - -local UI = {} - --- *************************************************************************** --- registered menu actions --- *************************************************************************** -function UI.call(func, parameter) - -- 'call' is fire-and-forget - leap.request('UI', {op='call', ['function']=func, parameter=parameter}) -end - -function UI.getValue(path) - return leap.request('UI', {op='getValue', path=path})['value'] -end - --- *************************************************************************** --- UI views --- *************************************************************************** --- Either: --- wreq{op='Something', a=1, b=2, ...} --- or: --- (args should be local, as this wreq() call modifies it) --- local args = {a=1, b=2, ...} --- wreq('Something', args) -local function wreq(op_or_data, data_if_op) - if data_if_op ~= nil then - -- this is the wreq(op, data) form - data_if_op.op = op_or_data - op_or_data = data_if_op - end - return leap.request('LLWindow', op_or_data) -end - --- omit 'parent' to list all view paths -function UI.listviews(parent) - return wreq{op='getPaths', under=parent} -end - -function UI.viewinfo(path) - return wreq{op='getInfo', path=path} -end - --- *************************************************************************** --- mouse actions --- *************************************************************************** --- pass a table: --- UI.click{path=path --- [, button='LEFT' | 'CENTER' | 'RIGHT'] --- [, x=x, y=y] --- [, hold=duration]} -function UI.click(...) - local args = mapargs('path,button,x,y,hold', ...) - args.button = args.button or 'LEFT' - local hold = args.hold or 1.0 - wreq('mouseMove', args) - wreq('mouseDown', args) - Timer(hold, 'wait') - wreq('mouseUp', args) -end - --- pass a table as for UI.click() -function UI.doubleclick(...) - local args = mapargs('path,button,x,y', ...) - args.button = args.button or 'LEFT' - wreq('mouseDown', args) - wreq('mouseUp', args) - wreq('mouseDown', args) - wreq('mouseUp', args) -end - --- UI.drag{path=, xoff=, yoff=} -function UI.drag(...) - local args = mapargs('path,xoff,yoff', ...) - -- query the specified path - local rect = UI.viewinfo(args.path).rect - local centerx = math.floor(rect.left + (rect.right - rect.left)/2) - local centery = math.floor(rect.bottom + (rect.top - rect.bottom)/2) - wreq{op='mouseMove', path=args.path, x=centerx, y=centery} - wreq{op='mouseDown', path=args.path, button='LEFT'} - wreq{op='mouseMove', path=args.path, x=centerx + args.xoff, y=centery + args.yoff} - wreq{op='mouseUp', path=args.path, button='LEFT'} -end - --- *************************************************************************** --- keyboard actions --- *************************************************************************** --- pass a table: --- UI.keypress{ --- [path=path] -- if omitted, default input field --- [, char='x'] -- requires one of char, keycode, keysym --- [, keycode=120] --- keysym per https://github.com/secondlife/viewer/blob/main/indra/llwindow/llkeyboard.cpp#L68-L124 --- [, keysym='Enter'] --- [, mask={'SHIFT', 'CTL', 'ALT', 'MAC_CONTROL'}] -- some subset of these --- } -function UI.keypress(...) - local args = mapargs('path,char,keycode,keysym,mask', ...) - if args.char == '\n' then - args.char = nil - args.keysym = 'Enter' - end - return wreq('keyDown', args) -end - --- UI.type{text=, path=} -function UI.type(...) - local args = mapargs('text,path', ...) - if #args.text > 0 then - -- The caller's path may be specified in a way that requires recursively - -- searching parts of the LLView tree. No point in doing that more than - -- once. Capture the actual path found by that first call and use that for - -- subsequent calls. - local path = UI.keypress{path=args.path, char=string.sub(args.text, 1, 1)}.path - for i = 2, #args.text do - UI.keypress{path=path, char=string.sub(args.text, i, i)} - end - end -end - -return UI diff --git a/indra/newview/scripts/lua/WaitQueue.lua b/indra/newview/scripts/lua/WaitQueue.lua deleted file mode 100644 index 7e10d03295..0000000000 --- a/indra/newview/scripts/lua/WaitQueue.lua +++ /dev/null @@ -1,88 +0,0 @@ --- 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 fiber = require('fiber') -local Queue = require('Queue') -local util = require('util') - -local function dbg(...) end --- local dbg = require('printf') - -local WaitQueue = Queue() - -function WaitQueue:new() - local obj = Queue() - setmetatable(obj, self) - self.__index = self - - obj._waiters = {} - obj._closed = false - return obj -end - -util.classctor(WaitQueue) - -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 wake only one of the waiting Dequeue() - -- callers. - if ((not self:IsEmpty()) or self._closed) and next(self._waiters) then - -- Pop the oldest waiting coroutine instead of the most recent, for - -- more-or-less round robin fairness. But skip any coroutines that - -- have gone dead in the meantime. - local waiter = table.remove(self._waiters, 1) - while waiter and fiber.status(waiter) == "dead" do - waiter = table.remove(self._waiters, 1) - end - -- do we still have at least one waiting coroutine? - if waiter then - -- don't pass the head item: let the resumed coroutine retrieve it - fiber.wake(waiter) - 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 - dbg('WaitQueue:Dequeue(): closed') - return nil - end - dbg('WaitQueue:Dequeue(): waiting') - -- add the running coroutine to the list of waiters - dbg('WaitQueue:Dequeue() running %s', tostring(coroutine.running() or 'main')) - table.insert(self._waiters, fiber.running()) - -- then let somebody else run - fiber.wait() - end - -- here we're sure this queue isn't empty - dbg('WaitQueue:Dequeue() calling Queue.Dequeue()') - 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/coro.lua b/indra/newview/scripts/lua/coro.lua deleted file mode 100644 index 616a797e95..0000000000 --- a/indra/newview/scripts/lua/coro.lua +++ /dev/null @@ -1,67 +0,0 @@ --- Manage Lua coroutines - -local coro = {} - -coro._coros = {} - --- Launch a Lua coroutine: create and resume. --- Returns: new coroutine, values yielded or returned from initial resume() --- If initial resume() encountered an error, propagates the error. -function coro.launch(func, ...) - local co = coroutine.create(func) - table.insert(coro._coros, co) - return co, coro.resume(co, ...) -end - --- resume() wrapper to propagate errors -function coro.resume(co, ...) - -- if there's an idiom other than table.pack() to assign an arbitrary - -- number of return values, I don't yet know it - local ok_result = table.pack(coroutine.resume(co, ...)) - if not ok_result[1] then - -- if [1] is false, then [2] is the error message - error(ok_result[2]) - end - -- ok is true, whew, just return the rest of the values - return table.unpack(ok_result, 2) -end - --- yield to other coroutines even if you don't know whether you're in a --- created coroutine or the main coroutine -function coro.yield(...) - if coroutine.running() then - -- this is a real coroutine, yield normally - return coroutine.yield(...) - else - -- This is the main coroutine: coroutine.yield() doesn't work. - -- But we can take a spin through previously-launched coroutines. - -- Walk a copy of coro._coros in case any of these coroutines launches - -- another: next() forbids creating new entries during traversal. - for co in coro._live_coros_iter, table.clone(coro._coros) do - coro.resume(co) - end - end -end - --- Walk coro._coros table, returning running or suspended coroutines. --- Once a coroutine becomes dead, remove it from _coros and don't return it. -function coro._live_coros() - return coro._live_coros_iter, coro._coros -end - --- iterator function for _live_coros() -function coro._live_coros_iter(t, idx) - local k, co = next(t, idx) - while k and coroutine.status(co) == 'dead' do --- t[k] = nil - -- See coro.yield(): sometimes we traverse a copy of _coros, but if we - -- discover a dead coroutine in that copy, delete it from _coros - -- anyway. Deleting it from a temporary copy does nothing. - coro._coros[k] = nil - coroutine.close(co) - k, co = next(t, k) - end - return co -end - -return coro diff --git a/indra/newview/scripts/lua/fiber.lua b/indra/newview/scripts/lua/fiber.lua deleted file mode 100644 index cae27b936b..0000000000 --- a/indra/newview/scripts/lua/fiber.lua +++ /dev/null @@ -1,340 +0,0 @@ --- Organize Lua coroutines into fibers. - --- In this usage, the difference between coroutines and fibers is that fibers --- have a scheduler. Yielding a fiber means allowing other fibers, plural, to --- run: it's more than just returning control to the specific Lua thread that --- resumed the running coroutine. - --- fiber.launch() creates a new fiber ready to run. --- fiber.status() reports (augmented) status of the passed fiber: instead of --- 'suspended', it returns either 'ready' or 'waiting' --- fiber.yield() allows other fibers to run, but leaves the calling fiber --- ready to run. --- fiber.wait() marks the running fiber not ready, and resumes other fibers. --- fiber.wake() marks the designated suspended fiber ready to run, but does --- not yet resume it. --- fiber.run() runs all current fibers until all have terminated (successfully --- or with an error). - -local printf = require 'printf' -local function dbg(...) end --- local dbg = printf -local coro = require 'coro' - -local fiber = {} - --- The tables in which we track fibers must have weak keys so dead fibers --- can be garbage-collected. -local weak_values = {__mode='v'} -local weak_keys = {__mode='k'} - --- Track each current fiber as being either ready to run or not ready --- (waiting). wait() moves the running fiber from ready to waiting; wake() --- moves the designated fiber from waiting back to ready. --- The ready table is used as a list so yield() can go round robin. -local ready = setmetatable({'main'}, weak_keys) --- The waiting table is used as a set because order doesn't matter. -local waiting = setmetatable({}, weak_keys) - --- Every fiber has a name, for diagnostic purposes. Names must be unique. --- A colliding name will be suffixed with an integer. --- Predefine 'main' with our marker so nobody else claims that name. -local names = setmetatable({main='main'}, weak_keys) -local byname = setmetatable({main='main'}, weak_values) --- each colliding name has its own distinct suffix counter -local suffix = {} - --- Specify a nullary idle() callback to be called whenever there are no ready --- fibers but there are waiting fibers. The idle() callback is responsible for --- changing zero or more waiting fibers to ready fibers by calling --- fiber.wake(), although a given call may leave them all still waiting. --- When there are no ready fibers, it's a good idea for the idle() function to --- return control to a higher-level execution agent. Simply returning without --- changing any fiber's status will spin the CPU. --- The idle() callback can return non-nil to exit fiber.run() with that value. -function fiber._idle() - error('fiber.yield(): you must first call set_idle(nullary idle() function)') -end - -function fiber.set_idle(func) - fiber._idle = func -end - --- Launch a new Lua fiber, ready to run. -function fiber.launch(name, func, ...) - local args = table.pack(...) - local co = coroutine.create(function() func(table.unpack(args)) end) - -- a new fiber is ready to run - table.insert(ready, co) - local namekey = name - while byname[namekey] do - if not suffix[name] then - suffix[name] = 1 - end - suffix[name] += 1 - namekey = name .. tostring(suffix[name]) - end - -- found a namekey not yet in byname: set it - byname[namekey] = co - -- and remember it as this fiber's name - names[co] = namekey --- dbg('launch(%s)', namekey) --- dbg('byname[%s] = %s', namekey, tostring(byname[namekey])) --- dbg('names[%s] = %s', tostring(co), names[co]) --- dbg('ready[-1] = %s', tostring(ready[#ready])) -end - --- for debugging -function format_all() - output = {} - table.insert(output, 'Ready fibers:' .. if next(ready) then '' else ' none') - for _, co in pairs(ready) do - table.insert(output, string.format(' %s: %s', fiber.get_name(co), fiber.status(co))) - end - table.insert(output, 'Waiting fibers:' .. if next(waiting) then '' else ' none') - for co in pairs(waiting) do - table.insert(output, string.format(' %s: %s', fiber.get_name(co), fiber.status(co))) - end - return table.concat(output, '\n') -end - -function fiber.print_all() - print(format_all()) -end - --- return either the running coroutine or, if called from the main thread, --- 'main' -function fiber.running() - return coroutine.running() or 'main' -end - --- Query a fiber's name (nil for the running fiber) -function fiber.get_name(co) - return names[co or fiber.running()] or 'unknown' -end - --- Query status of the passed fiber -function fiber.status(co) - local running = coroutine.running() - if (not co) or co == running then - -- silly to ask the status of the running fiber: it's 'running' - return 'running' - end - if co ~= 'main' then - -- for any coroutine but main, consult coroutine.status() - local status = coroutine.status(co) - if status ~= 'suspended' then - return status - end - -- here co is suspended, answer needs further refinement - else - -- co == 'main' - if not running then - -- asking about 'main' from the main fiber - return 'running' - end - -- asking about 'main' from some other fiber, so presumably main is suspended - end - -- here we know co is suspended -- but is it ready to run? - if waiting[co] then - return 'waiting' - end - -- not waiting should imply ready: sanity check - if table.find(ready, co) then - return 'ready' - end - -- Calls within yield() between popping the next ready fiber and - -- re-appending it to the list are in this state. Once we're done - -- debugging yield(), we could reinstate either of the below. --- error(string.format('fiber.status(%s) is stumped', fiber.get_name(co))) --- print(string.format('*** fiber.status(%s) is stumped', fiber.get_name(co))) - return '(unknown)' -end - --- change the running fiber's status to waiting -local function set_waiting() - -- if called from the main fiber, inject a 'main' marker into the list - co = fiber.running() - -- delete from ready list - local i = table.find(ready, co) - if i then - table.remove(ready, i) - end - -- add to waiting list - waiting[co] = true -end - --- Suspend the current fiber until some other fiber calls fiber.wake() on it -function fiber.wait() - dbg('Fiber %q waiting', fiber.get_name()) - set_waiting() - -- now yield to other fibers - fiber.yield() -end - --- Mark a suspended fiber as being ready to run -function fiber.wake(co) - if not waiting[co] then - error(string.format('fiber.wake(%s) but status=%s, ready=%s, waiting=%s', - names[co], fiber.status(co), ready[co], waiting[co])) - end - -- delete from waiting list - waiting[co] = nil - -- add to end of ready list - table.insert(ready, co) - dbg('Fiber %q ready', fiber.get_name(co)) - -- but don't yet resume it: that happens next time we reach yield() -end - --- pop and return the next not-dead fiber in the ready list, or nil if none remain -local function live_ready_iter() - -- don't write: - -- for co in table.remove, ready, 1 - -- because it would keep passing a new second parameter! - for co in function() return table.remove(ready, 1) end do - dbg('%s live_ready_iter() sees %s, status %s', - fiber.get_name(), fiber.get_name(co), fiber.status(co)) - -- keep removing the head entry until we find one that's not dead, - -- discarding any dead coroutines along the way - if co == 'main' or coroutine.status(co) ~= 'dead' then - dbg('%s live_ready_iter() returning %s', - fiber.get_name(), fiber.get_name(co)) - return co - end - end - dbg('%s live_ready_iter() returning nil', fiber.get_name()) - return nil -end - --- prune the set of waiting fibers -local function prune_waiting() - for waiter in pairs(waiting) do - if waiter ~= 'main' and coroutine.status(waiter) == 'dead' then - waiting[waiter] = nil - end - end -end - --- Run other ready fibers, leaving this one ready, returning after a cycle. --- Returns: --- * true, nil if there remain other live fibers, whether ready or waiting, --- but it's our turn to run --- * false, nil if this is the only remaining fiber --- * nil, x if configured idle() callback returns non-nil x -local function scheduler() - dbg('scheduler():\n%s', format_all()) - -- scheduler() is asymmetric because Lua distinguishes the main thread - -- from other coroutines. The main thread can't yield; it can only resume - -- other coroutines. So although an arbitrary coroutine could resume still - -- other arbitrary coroutines, it could NOT resume the main thread because - -- the main thread can't yield. Therefore, scheduler() delegates its real - -- processing to the main thread. If called from a coroutine, pass control - -- back to the main thread. - if coroutine.running() then - -- this is a real coroutine, yield normally to main thread - coroutine.yield() - -- main certainly still exists - return true - end - - -- This is the main fiber: coroutine.yield() doesn't work. - -- Instead, resume each of the ready fibers. - -- Prune the set of waiting fibers after every time fiber business logic - -- runs (i.e. other fibers might have terminated or hit error), such as - -- here on entry. - prune_waiting() - local others, idle_stop - repeat - for co in live_ready_iter do - -- seize the opportunity to make sure the viewer isn't shutting down - LL.check_stop() - -- before we re-append co, is it the only remaining entry? - others = next(ready) - -- co is live, re-append it to the ready list - table.insert(ready, co) - if co == 'main' then - -- Since we know the caller is the main fiber, it's our turn. - -- Tell caller if there are other ready or waiting fibers. - return others or next(waiting) - end - -- not main, but some other ready coroutine: - -- use coro.resume() so we'll propagate any error encountered - coro.resume(co) - prune_waiting() - end - -- Here there are no ready fibers. Are there any waiting fibers? - if not next(waiting) then - return false - end - -- there are waiting fibers: call consumer's configured idle() function - idle_stop = fiber._idle() - if idle_stop ~= nil then - return nil, idle_stop - end - prune_waiting() - -- loop "forever", that is, until: - -- * main is ready, or - -- * there are neither ready fibers nor waiting fibers, or - -- * fiber._idle() returned non-nil - until false -end - --- Let other fibers run. This is useful in either of two cases: --- * fiber.wait() calls this to run other fibers while this one is waiting. --- fiber.yield() (and therefore fiber.wait()) works from the main thread as --- well as from explicitly-launched fibers, without the caller having to --- care. --- * A long-running fiber that doesn't often call fiber.wait() should sprinkle --- in fiber.yield() calls to interleave processing on other fibers. -function fiber.yield() - -- The difference between this and fiber.run() is that fiber.yield() - -- assumes its caller has work to do. yield() returns to its caller as - -- soon as scheduler() pops this fiber from the ready list. fiber.run() - -- continues looping until all other fibers have terminated, or the - -- set_idle() callback tells it to stop. - local others, idle_done = scheduler() - -- scheduler() returns either if we're ready, or if idle_done ~= nil. - if idle_done ~= nil then - -- Returning normally from yield() means the caller can carry on with - -- its pending work. But in this case scheduler() returned because the - -- configured set_idle() function interrupted it -- not because we're - -- actually ready. Don't return normally. - error('fiber.set_idle() interrupted yield() with: ' .. tostring(idle_done)) - end - -- We're ready! Just return to caller. In this situation we don't care - -- whether there are other ready fibers. - dbg('fiber.yield() returning to %s (%sothers are ready)', - fiber.get_name(), ((not others) and "no " or "")) -end - --- Run fibers until all but main have terminated: return nil. --- Or until configured idle() callback returns x ~= nil: return x. -function fiber.run() - -- A fiber calling run() is not also doing other useful work. Remove the - -- calling fiber from the ready list. Otherwise yield() would keep seeing - -- that our caller is ready and return to us, instead of realizing that - -- all coroutines are waiting and call idle(). But don't say we're - -- waiting, either, because then when all other fibers have terminated - -- we'd call idle() forever waiting for something to make us ready again. - local i = table.find(ready, fiber.running()) - if i then - table.remove(ready, i) - end - local others, idle_done - repeat - dbg('%s calling fiber.run() calling scheduler()', fiber.get_name()) - others, idle_done = scheduler() - dbg("%s fiber.run()'s scheduler() returned %s, %s", fiber.get_name(), - tostring(others), tostring(idle_done)) - until (not others) - dbg('%s fiber.run() done', fiber.get_name()) - -- For whatever it's worth, put our own fiber back in the ready list. - table.insert(ready, fiber.running()) - -- Once there are no more waiting fibers, and the only ready fiber is - -- us, return to caller. All previously-launched fibers are done. Possibly - -- the chunk is done, or the chunk may decide to launch a new batch of - -- fibers. - return idle_done -end - -return fiber diff --git a/indra/newview/scripts/lua/inspect.lua b/indra/newview/scripts/lua/inspect.lua deleted file mode 100644 index 9900a0b81b..0000000000 --- a/indra/newview/scripts/lua/inspect.lua +++ /dev/null @@ -1,371 +0,0 @@ -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, ' = ') - 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 deleted file mode 100644 index 82f91ce9e9..0000000000 --- a/indra/newview/scripts/lua/leap.lua +++ /dev/null @@ -1,550 +0,0 @@ --- 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. --- --- Usage: --- 1. Launch some number of Lua coroutines. The code in each coroutine may --- call leap.send(), leap.request() or leap.generate(). leap.send() returns --- immediately ("fire and forget"). leap.request() blocks the calling --- coroutine until it receives and returns the viewer's response to its --- request. leap.generate() expects an arbitrary number of responses to the --- original request. --- 2. To handle events from the viewer other than direct responses to --- requests, instantiate a leap.WaitFor object with a filter(pump, data) --- override method that returns non-nil for desired events. A coroutine may --- call wait() on any such WaitFor. --- 3. Once the coroutines have been launched, call leap.process() on the main --- coroutine. process() retrieves incoming events from the viewer and --- dispatches them to waiting request() or generate() calls, or to --- appropriate WaitFor instances. process() returns when either --- get_event_next() raises an error or the viewer posts nil to the script's --- reply pump to indicate it's done. --- 4. Alternatively, a running coroutine may call leap.done() to break out of --- leap.process(). process() won't notice until the next event from the --- viewer, though. - -local fiber = require('fiber') -local ErrorQueue = require('ErrorQueue') -local inspect = require('inspect') -local function dbg(...) end --- local dbg = require('printf') -local util = require('util') - -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. -local reply, command = LL.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 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. --- pending is NOT a weak table because the caller of request() or generate() --- never sees the WaitForReqid object. pending holds the only reference, so --- it should NOT be garbage-collected. -local 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. --- Anyone who instantiates a WaitFor subclass object should retain a reference --- to it. Once the consuming script drops the reference, allow Lua to --- garbage-collect the WaitFor despite its entry in waitfors. -local weak_values = {__mode='v'} -local waitfors = setmetatable({}, weak_values) --- 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 --- break leap.process() loop -leap._done = false - --- get the name of the reply pump -function leap.replypump() - return reply -end - --- get the name of the command pump -function leap.cmdpump() - return 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'] = reply - if reqid ~= nil then - data['reqid'] = reqid - end - end - dbg('leap.send(%s, %s) calling post_on()', pump, data) - LL.post_on(pump, data) -end - --- common setup code shared by request() and generate() -local function requestSetup(pump, data) - -- invent a new, unique reqid - leap._reqid += 1 - local reqid = leap._reqid - -- Instantiate a new WaitForReqid object. The priority is irrelevant - -- because, unlike the WaitFor base class, WaitForReqid does not - -- self-register on our waitfors list. Instead, capture the new - -- WaitForReqid object in pending so dispatch() can find it. - local waitfor = leap.WaitForReqid(reqid) - pending[reqid] = waitfor - -- Pass reqid to send() to stamp it into (a copy of) the request data. - dbg('requestSetup(%s, %s) storing %s', pump, data, waitfor.name) - leap.send(pump, data, reqid) - return reqid, waitfor -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. --- --- See also send(), generate(). -function leap.request(pump, data) - local reqid, waitfor = requestSetup(pump, data) - dbg('leap.request(%s, %s) about to wait on %s', pump, data, tostring(waitfor)) - local ok, response = pcall(waitfor.wait, waitfor) - dbg('leap.request(%s, %s) got %s: %s', pump, data, ok, response) - -- kill off temporary WaitForReqid object, even if error - pending[reqid] = nil - if not ok then - error(response) - elseif response.error then - error(response.error) - else - return response - end -end - --- Send the specified request LLSD, expecting an arbitrary number of replies. --- Each one is returned on request. --- --- Usage: --- sequence = leap.generate(pump, data) --- repeat --- response = sequence.next() --- until last(response) --- (last() means whatever test the caller wants to perform on response) --- sequence.done() --- --- See request() remarks about ["reqid"]. --- --- Note: this seems like a prime use case for Lua coroutines. But in a script --- using fibers.lua, a "wild" coroutine confuses the fiber scheduler. If --- generate() were itself a coroutine, it would call WaitForReqid:wait(), --- which would yield -- thereby resuming generate() WITHOUT waiting. -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, waitfor = requestSetup(pump, data) - return { - next = function() - dbg('leap.generate(%s).next() about to wait on %s', reqid, tostring(waitfor)) - local ok, response = pcall(waitfor.wait, waitfor) - dbg('leap.generate(%s).next() got %s: %s', reqid, ok, response) - if not ok then - error(response) - elseif response.error then - error(response.error) - else - return response - end - end, - done = function() - -- cleanup consists of removing our WaitForReqid from pending - pending[reqid] = nil - end - } -end - --- Send the specified request LLSD, expecting an immediate reply followed by --- an arbitrary number of subsequent replies with the same reqid. Block the --- calling coroutine until the first (immediate) reply, but launch a separate --- fiber on which to call the passed callback with later replies. --- --- Once the callback returns true, the background fiber terminates. -function leap.eventstream(pump, data, callback) - local reqid, waitfor = requestSetup(pump, data) - local response = waitfor:wait() - if response.error then - -- clean up our WaitForReqid - waitfor:close() - error(response.error) - end - -- No error, so far so good: - -- call the callback with the first response just in case - dbg('leap.eventstream(%s): first callback', reqid) - local ok, done = pcall(callback, response) - dbg('leap.eventstream(%s) got %s, %s', reqid, ok, done) - if not ok then - -- clean up our WaitForReqid - waitfor:close() - error(done) - end - if done then - return response - end - -- callback didn't throw an error, and didn't say stop, - -- so set up to handle subsequent events - -- TODO: distinguish "daemon" fibers that can be terminated even if waiting - fiber.launch( - pump, - function () - local ok, done - local nth = 1 - repeat - event = waitfor:wait() - if not event then - -- wait() returns nil once the queue is closed (e.g. cancelreq()) - ok, done = true, true - else - nth += 1 - dbg('leap.eventstream(%s): callback %d', reqid, nth) - ok, done = pcall(callback, event) - dbg('leap.eventstream(%s) got %s, %s', reqid, ok, done) - end - -- not ok means callback threw an error (caught as 'done') - -- done means callback succeeded but wants to stop - until (not ok) or done - -- once we break this loop, clean up our WaitForReqid - waitfor:close() - if not ok then - -- can't reflect the error back to our caller - LL.print_warning(fiber.get_name() .. ': ' .. done) - end - end) - return response -end - --- we might want to clean up after leap.eventstream() even if the callback has --- not yet returned true -function leap.cancelreq(reqid) - dbg('cancelreq(%s)', reqid) - local waitfor = pending[reqid] - if waitfor ~= nil then - -- close() removes the pending entry and also closes the queue, - -- breaking the background fiber's wait loop. - dbg('cancelreq(%s) canceling %s', reqid, waitfor.name) - waitfor:close() - end -end - -local function cleanup(message) - -- We're done: clean up all pending coroutines. - -- Iterate over copies of the pending and waitfors tables, since the - -- close() operation modifies the real tables. - for i, waitfor in pairs(table.clone(pending)) do - waitfor:close() - end - for i, waitfor in pairs(table.clone(waitfors)) do - waitfor:close() - end -end - --- Handle an incoming (pump, data) event with no recognizable ['reqid'] -local function 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(waitfors) do - dbg('unsolicited() checking %s', waitfor.name) - if waitfor:handle(pump, data) then - return - end - end - LL.print_debug(string.format('unsolicited(%s, %s) discarding unclaimed event', - pump, inspect(data))) -end - --- Route incoming (pump, data) event to the appropriate waiting coroutine. -local function dispatch(pump, data) - local reqid = data['reqid'] - -- if the response has no 'reqid', it's not from request() or generate() - if reqid == nil then --- dbg('dispatch() found no reqid; calling unsolicited(%s, %s)', pump, data) - return unsolicited(pump, data) - end - -- have reqid; do we have a WaitForReqid? - local waitfor = pending[reqid] - if waitfor == nil then --- dbg('dispatch() found no WaitForReqid(%s); calling unsolicited(%s, %s)', reqid, pump, data) - return unsolicited(pump, data) - end - -- found the right WaitForReqid object, let it handle the event --- dbg('dispatch() calling %s.handle(%s, %s)', waitfor.name, pump, data) - waitfor:handle(pump, data) -end - --- We configure fiber.set_idle() function. fiber.yield() calls the configured --- idle callback whenever there are waiting fibers but no ready fibers. In --- our case, that means it's time to fetch another incoming viewer event. -fiber.set_idle(function () - -- If someone has called leap.done(), then tell fiber.yield() to break loop. - if leap._done then - cleanup('done') - return 'done' - end - dbg('leap.idle() calling get_event_next()') - local ok, pump, data = pcall(LL.get_event_next) - dbg('leap.idle() got %s: %s, %s', ok, pump, data) - -- ok false means get_event_next() raised a Lua error, pump is message - if not ok then - cleanup(pump) - error(pump) - end - -- data nil means get_event_next() returned (pump, LLSD()) to indicate done - if not data then - cleanup('end') - return 'end' - end - -- got a real pump, data pair - dispatch(pump, data) - -- return to fiber.yield(): any incoming message might result in one or - -- more fibers becoming ready -end) - -function leap.done() - leap._done = true -end - --- called by WaitFor.enable() -local function registerWaitFor(waitfor) - table.insert(waitfors, waitfor) - -- keep waitfors sorted in descending order of specified priority - table.sort(waitfors, - function (lhs, rhs) return lhs.priority > rhs.priority end) -end - --- called by WaitFor.disable() -local function unregisterWaitFor(waitfor) - local i = table.find(waitfors, waitfor) - if i ~= nil then - waitfors[i] = nil - 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.tostring(self) - -- Lua (sub)classes have no name; can't prefix with that - return self.name -end - -function leap.WaitFor:new(priority, name) - local obj = setmetatable({__tostring=leap.WaitFor.tostring}, 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() - obj._registered = false - -- if no priority, then don't enable() - remember 0 is truthy - if priority then - obj:enable() - end - - return obj -end - -util.classctor(leap.WaitFor) - --- Re-enable a disable()d WaitFor object. New WaitFor objects are --- enable()d by default. -function leap.WaitFor:enable() - if not self._registered then - registerWaitFor(self) - self._registered = true - end -end - --- Disable an enable()d WaitFor object. -function leap.WaitFor:disable() - if self._registered then - 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() - dbg('%s about to wait', self.name) - local item = self._queue:Dequeue() - dbg('%s got %s', self.name, item) - return item -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 override the WaitFor.filter() method') -end - --- called by unsolicited() for each WaitFor in waitfors -function leap.WaitFor:handle(pump, data) - local item = self:filter(pump, data) - dbg('%s.filter() returned %s', self.name, item) - -- 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 cleanup() at end -function leap.WaitFor:close() - self:disable() - self._queue:close() -end - --- called by leap.process() when get_event_next() raises an error -function leap.WaitFor:exception(message) - LL.print_warning(self.name .. ' error: ' .. message) - self._queue:Error(message) -end - --- ------------------------------ WaitForReqid ------------------------------- -leap.WaitForReqid = leap.WaitFor() - -function leap.WaitForReqid:new(reqid) - -- priority is meaningless, since this object won't be added to the - -- priority-sorted waitfors list. Use the reqid as the debugging name - -- string. - local obj = leap.WaitFor(nil, 'WaitForReqid(' .. reqid .. ')') - setmetatable(obj, self) - self.__index = self - - obj.reqid = reqid - - return obj -end - -util.classctor(leap.WaitForReqid) - -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 - -function leap.WaitForReqid:close() - -- remove this entry from pending table - pending[self.reqid] = nil - self._queue:close() -end - -return leap diff --git a/indra/newview/scripts/lua/printf.lua b/indra/newview/scripts/lua/printf.lua deleted file mode 100644 index e84b2024df..0000000000 --- a/indra/newview/scripts/lua/printf.lua +++ /dev/null @@ -1,19 +0,0 @@ --- printf(...) is short for print(string.format(...)) - -local inspect = require 'inspect' - -local function printf(format, ...) - -- string.format() only handles numbers and strings. - -- Convert anything else to string using the inspect module. - local args = {} - for _, arg in pairs(table.pack(...)) do - if type(arg) == 'number' or type(arg) == 'string' then - table.insert(args, arg) - else - table.insert(args, inspect(arg)) - end - end - print(string.format(format, table.unpack(args))) -end - -return printf diff --git a/indra/newview/scripts/lua/require/ErrorQueue.lua b/indra/newview/scripts/lua/require/ErrorQueue.lua new file mode 100644 index 0000000000..e6e9a5ef48 --- /dev/null +++ b/indra/newview/scripts/lua/require/ErrorQueue.lua @@ -0,0 +1,37 @@ +-- 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 function dbg(...) end +-- local dbg = require('printf') +local util = require('util') + +local ErrorQueue = WaitQueue() + +util.classctor(ErrorQueue) + +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. + dbg('Setting self._closed to %q', message) + self._closed = message + self:_wake_waiters() +end + +function ErrorQueue:Dequeue() + local value = WaitQueue.Dequeue(self) + dbg('ErrorQueue:Dequeue: base Dequeue() got %s', value) + 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/require/Floater.lua b/indra/newview/scripts/lua/require/Floater.lua new file mode 100644 index 0000000000..d057a74386 --- /dev/null +++ b/indra/newview/scripts/lua/require/Floater.lua @@ -0,0 +1,146 @@ +-- Floater base class + +local leap = require 'leap' +local fiber = require 'fiber' +local util = require 'util' + +-- list of all the events that a LLLuaFloater might send +local event_list = leap.request("LLFloaterReg", {op="getFloaterEvents"}).events +local event_set = {} +for _, event in pairs(event_list) do + event_set[event] = true +end + +local function _event(event_name) + if not event_set[event_name] then + error("Incorrect event name: " .. event_name, 3) + end + return event_name +end + +-- --------------------------------------------------------------------------- +local Floater = {} + +-- Pass: +-- relative file path to floater's XUI definition file +-- optional: sign up for additional events for defined control +-- {={action1, action2, ...}} +function Floater:new(path, extra) + local obj = setmetatable({}, self) + self.__index = self + + local path_parts = string.split(path, '/') + obj.name = 'Floater ' .. path_parts[#path_parts] + + obj._command = {op="showLuaFloater", xml_path=LL.abspath(path)} + if extra then + -- validate each of the actions for each specified control + for control, actions in pairs(extra) do + for _, action in pairs(actions) do + _event(action) + end + end + obj._command.extra_events = extra + end + + return obj +end + +util.classctor(Floater) + +function Floater:show() + -- leap.eventstream() returns the first response, and launches a + -- background fiber to call the passed callback with all subsequent + -- responses. + local event = leap.eventstream( + 'LLFloaterReg', + self._command, + -- handleEvents() returns false when done. + -- eventstream() expects a true return when done. + function(event) return not self:handleEvents(event) end) + self._pump = event.command_name + -- we might need the returned reqid to cancel the eventstream() fiber + self.reqid = event.reqid + + -- The response to 'showLuaFloater' *is* the 'post_build' event. Check if + -- subclass has a post_build() method. Honor the convention that if + -- handleEvents() returns false, we're done. + if not self:handleEvents(event) then + return + end +end + +function Floater:post(action) + leap.send(self._pump, action) +end + +function Floater:request(action) + return leap.request(self._pump, action) +end + +-- local inspect = require 'inspect' + +function Floater:handleEvents(event_data) + local event = event_data.event + if event_set[event] == nil then + LL.print_warning(string.format('%s received unknown event %q', self.name, event)) + end + + -- Before checking for a general (e.g.) commit() method, first look for + -- commit_ctrl_name(): in other words, concatenate the event name with the + -- ctrl_name, with an underscore between. If there exists such a specific + -- method, call that. + local handler, ret + if event_data.ctrl_name then + local specific = event .. '_' .. event_data.ctrl_name + handler = self[specific] + if handler then + ret = handler(self, event_data) + -- Avoid 'return ret or true' because we explicitly want to allow + -- the handler to return false. + if ret ~= nil then + return ret + else + return true + end + end + end + + -- No specific "event_on_ctrl()" method found; try just "event()" + handler = self[event] + if handler then + ret = handler(self, event_data) + if ret ~= nil then + return ret + end +-- else +-- print(string.format('%s ignoring event %s', self.name, inspect(event_data))) + end + + -- We check for event() method before recognizing floater_close in case + -- the consumer needs to react specially to closing the floater. Now that + -- we've checked, recognize it ourselves. Returning false terminates the + -- anonymous fiber function launched by leap.eventstream(). + if event == _event('floater_close') then + LL.print_warning(self.name .. ' closed') + return false + end + return true +end + +-- onCtrl() permits a different dispatch style in which the general event() +-- method explicitly calls (e.g.) +-- self:onCtrl(event_data, { +-- ctrl_name=function() +-- self:post(...) +-- end, +-- ... +-- }) +function Floater:onCtrl(event_data, ctrl_map) + local handler = ctrl_map[event_data.ctrl_name] + if handler then + handler() + end +end + +return Floater diff --git a/indra/newview/scripts/lua/require/LLChat.lua b/indra/newview/scripts/lua/require/LLChat.lua new file mode 100644 index 0000000000..78dca765e8 --- /dev/null +++ b/indra/newview/scripts/lua/require/LLChat.lua @@ -0,0 +1,17 @@ +local leap = require 'leap' + +local LLChat = {} + +function LLChat.sendNearby(msg) + leap.send('LLChatBar', {op='sendChat', message=msg}) +end + +function LLChat.sendWhisper(msg) + leap.send('LLChatBar', {op='sendChat', type='whisper', message=msg}) +end + +function LLChat.sendShout(msg) + leap.send('LLChatBar', {op='sendChat', type='shout', message=msg}) +end + +return LLChat diff --git a/indra/newview/scripts/lua/require/LLDebugSettings.lua b/indra/newview/scripts/lua/require/LLDebugSettings.lua new file mode 100644 index 0000000000..cff1a63c21 --- /dev/null +++ b/indra/newview/scripts/lua/require/LLDebugSettings.lua @@ -0,0 +1,17 @@ +local leap = require 'leap' + +local LLDebugSettings = {} + +function LLDebugSettings.set(name, value) + leap.request('LLViewerControl', {op='set', group='Global', key=name, value=value}) +end + +function LLDebugSettings.toggle(name) + leap.request('LLViewerControl', {op='toggle', group='Global', key=name}) +end + +function LLDebugSettings.get(name) + return leap.request('LLViewerControl', {op='get', group='Global', key=name})['value'] +end + +return LLDebugSettings diff --git a/indra/newview/scripts/lua/require/LLFloaterAbout.lua b/indra/newview/scripts/lua/require/LLFloaterAbout.lua new file mode 100644 index 0000000000..a6e42d364f --- /dev/null +++ b/indra/newview/scripts/lua/require/LLFloaterAbout.lua @@ -0,0 +1,11 @@ +-- Engage the LLFloaterAbout LLEventAPI + +local leap = require 'leap' + +local LLFloaterAbout = {} + +function LLFloaterAbout.getInfo() + return leap.request('LLFloaterAbout', {op='getInfo'}) +end + +return LLFloaterAbout diff --git a/indra/newview/scripts/lua/require/LLGesture.lua b/indra/newview/scripts/lua/require/LLGesture.lua new file mode 100644 index 0000000000..343b611e2c --- /dev/null +++ b/indra/newview/scripts/lua/require/LLGesture.lua @@ -0,0 +1,23 @@ +-- Engage the LLGesture LLEventAPI + +local leap = require 'leap' + +local LLGesture = {} + +function LLGesture.getActiveGestures() + return leap.request('LLGesture', {op='getActiveGestures'})['gestures'] +end + +function LLGesture.isGesturePlaying(id) + return leap.request('LLGesture', {op='isGesturePlaying', id=id})['playing'] +end + +function LLGesture.startGesture(id) + leap.send('LLGesture', {op='startGesture', id=id}) +end + +function LLGesture.stopGesture(id) + leap.send('LLGesture', {op='stopGesture', id=id}) +end + +return LLGesture diff --git a/indra/newview/scripts/lua/require/Queue.lua b/indra/newview/scripts/lua/require/Queue.lua new file mode 100644 index 0000000000..5bc72e4057 --- /dev/null +++ b/indra/newview/scripts/lua/require/Queue.lua @@ -0,0 +1,51 @@ +-- from https://create.roblox.com/docs/luau/queues#implementing-queues, +-- amended per https://www.lua.org/pil/16.1.html + +-- While coding some scripting in Lua +-- I found that I needed a queua +-- I thought of linked list +-- But had to resist +-- For fear it might be too obscua. + +local util = require 'util' + +local Queue = {} + +function Queue:new() + local obj = setmetatable({}, self) + self.__index = self + + obj._first = 0 + obj._last = -1 + obj._queue = {} + + return obj +end + +util.classctor(Queue) + +-- Check if the queue is empty +function Queue:IsEmpty() + 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 +end + +-- Remove a value from the queue +function Queue:Dequeue() + 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/require/UI.lua b/indra/newview/scripts/lua/require/UI.lua new file mode 100644 index 0000000000..eb1a4017c7 --- /dev/null +++ b/indra/newview/scripts/lua/require/UI.lua @@ -0,0 +1,125 @@ +-- Engage the viewer's UI + +local leap = require 'leap' +local Timer = (require 'timers').Timer +local mapargs = require 'mapargs' + +local UI = {} + +-- *************************************************************************** +-- registered menu actions +-- *************************************************************************** +function UI.call(func, parameter) + -- 'call' is fire-and-forget + leap.request('UI', {op='call', ['function']=func, parameter=parameter}) +end + +function UI.getValue(path) + return leap.request('UI', {op='getValue', path=path})['value'] +end + +-- *************************************************************************** +-- UI views +-- *************************************************************************** +-- Either: +-- wreq{op='Something', a=1, b=2, ...} +-- or: +-- (args should be local, as this wreq() call modifies it) +-- local args = {a=1, b=2, ...} +-- wreq('Something', args) +local function wreq(op_or_data, data_if_op) + if data_if_op ~= nil then + -- this is the wreq(op, data) form + data_if_op.op = op_or_data + op_or_data = data_if_op + end + return leap.request('LLWindow', op_or_data) +end + +-- omit 'parent' to list all view paths +function UI.listviews(parent) + return wreq{op='getPaths', under=parent} +end + +function UI.viewinfo(path) + return wreq{op='getInfo', path=path} +end + +-- *************************************************************************** +-- mouse actions +-- *************************************************************************** +-- pass a table: +-- UI.click{path=path +-- [, button='LEFT' | 'CENTER' | 'RIGHT'] +-- [, x=x, y=y] +-- [, hold=duration]} +function UI.click(...) + local args = mapargs('path,button,x,y,hold', ...) + args.button = args.button or 'LEFT' + local hold = args.hold or 1.0 + wreq('mouseMove', args) + wreq('mouseDown', args) + Timer(hold, 'wait') + wreq('mouseUp', args) +end + +-- pass a table as for UI.click() +function UI.doubleclick(...) + local args = mapargs('path,button,x,y', ...) + args.button = args.button or 'LEFT' + wreq('mouseDown', args) + wreq('mouseUp', args) + wreq('mouseDown', args) + wreq('mouseUp', args) +end + +-- UI.drag{path=, xoff=, yoff=} +function UI.drag(...) + local args = mapargs('path,xoff,yoff', ...) + -- query the specified path + local rect = UI.viewinfo(args.path).rect + local centerx = math.floor(rect.left + (rect.right - rect.left)/2) + local centery = math.floor(rect.bottom + (rect.top - rect.bottom)/2) + wreq{op='mouseMove', path=args.path, x=centerx, y=centery} + wreq{op='mouseDown', path=args.path, button='LEFT'} + wreq{op='mouseMove', path=args.path, x=centerx + args.xoff, y=centery + args.yoff} + wreq{op='mouseUp', path=args.path, button='LEFT'} +end + +-- *************************************************************************** +-- keyboard actions +-- *************************************************************************** +-- pass a table: +-- UI.keypress{ +-- [path=path] -- if omitted, default input field +-- [, char='x'] -- requires one of char, keycode, keysym +-- [, keycode=120] +-- keysym per https://github.com/secondlife/viewer/blob/main/indra/llwindow/llkeyboard.cpp#L68-L124 +-- [, keysym='Enter'] +-- [, mask={'SHIFT', 'CTL', 'ALT', 'MAC_CONTROL'}] -- some subset of these +-- } +function UI.keypress(...) + local args = mapargs('path,char,keycode,keysym,mask', ...) + if args.char == '\n' then + args.char = nil + args.keysym = 'Enter' + end + return wreq('keyDown', args) +end + +-- UI.type{text=, path=} +function UI.type(...) + local args = mapargs('text,path', ...) + if #args.text > 0 then + -- The caller's path may be specified in a way that requires recursively + -- searching parts of the LLView tree. No point in doing that more than + -- once. Capture the actual path found by that first call and use that for + -- subsequent calls. + local path = UI.keypress{path=args.path, char=string.sub(args.text, 1, 1)}.path + for i = 2, #args.text do + UI.keypress{path=path, char=string.sub(args.text, i, i)} + end + end +end + +return UI diff --git a/indra/newview/scripts/lua/require/WaitQueue.lua b/indra/newview/scripts/lua/require/WaitQueue.lua new file mode 100644 index 0000000000..7e10d03295 --- /dev/null +++ b/indra/newview/scripts/lua/require/WaitQueue.lua @@ -0,0 +1,88 @@ +-- 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 fiber = require('fiber') +local Queue = require('Queue') +local util = require('util') + +local function dbg(...) end +-- local dbg = require('printf') + +local WaitQueue = Queue() + +function WaitQueue:new() + local obj = Queue() + setmetatable(obj, self) + self.__index = self + + obj._waiters = {} + obj._closed = false + return obj +end + +util.classctor(WaitQueue) + +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 wake only one of the waiting Dequeue() + -- callers. + if ((not self:IsEmpty()) or self._closed) and next(self._waiters) then + -- Pop the oldest waiting coroutine instead of the most recent, for + -- more-or-less round robin fairness. But skip any coroutines that + -- have gone dead in the meantime. + local waiter = table.remove(self._waiters, 1) + while waiter and fiber.status(waiter) == "dead" do + waiter = table.remove(self._waiters, 1) + end + -- do we still have at least one waiting coroutine? + if waiter then + -- don't pass the head item: let the resumed coroutine retrieve it + fiber.wake(waiter) + 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 + dbg('WaitQueue:Dequeue(): closed') + return nil + end + dbg('WaitQueue:Dequeue(): waiting') + -- add the running coroutine to the list of waiters + dbg('WaitQueue:Dequeue() running %s', tostring(coroutine.running() or 'main')) + table.insert(self._waiters, fiber.running()) + -- then let somebody else run + fiber.wait() + end + -- here we're sure this queue isn't empty + dbg('WaitQueue:Dequeue() calling Queue.Dequeue()') + 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/require/coro.lua b/indra/newview/scripts/lua/require/coro.lua new file mode 100644 index 0000000000..616a797e95 --- /dev/null +++ b/indra/newview/scripts/lua/require/coro.lua @@ -0,0 +1,67 @@ +-- Manage Lua coroutines + +local coro = {} + +coro._coros = {} + +-- Launch a Lua coroutine: create and resume. +-- Returns: new coroutine, values yielded or returned from initial resume() +-- If initial resume() encountered an error, propagates the error. +function coro.launch(func, ...) + local co = coroutine.create(func) + table.insert(coro._coros, co) + return co, coro.resume(co, ...) +end + +-- resume() wrapper to propagate errors +function coro.resume(co, ...) + -- if there's an idiom other than table.pack() to assign an arbitrary + -- number of return values, I don't yet know it + local ok_result = table.pack(coroutine.resume(co, ...)) + if not ok_result[1] then + -- if [1] is false, then [2] is the error message + error(ok_result[2]) + end + -- ok is true, whew, just return the rest of the values + return table.unpack(ok_result, 2) +end + +-- yield to other coroutines even if you don't know whether you're in a +-- created coroutine or the main coroutine +function coro.yield(...) + if coroutine.running() then + -- this is a real coroutine, yield normally + return coroutine.yield(...) + else + -- This is the main coroutine: coroutine.yield() doesn't work. + -- But we can take a spin through previously-launched coroutines. + -- Walk a copy of coro._coros in case any of these coroutines launches + -- another: next() forbids creating new entries during traversal. + for co in coro._live_coros_iter, table.clone(coro._coros) do + coro.resume(co) + end + end +end + +-- Walk coro._coros table, returning running or suspended coroutines. +-- Once a coroutine becomes dead, remove it from _coros and don't return it. +function coro._live_coros() + return coro._live_coros_iter, coro._coros +end + +-- iterator function for _live_coros() +function coro._live_coros_iter(t, idx) + local k, co = next(t, idx) + while k and coroutine.status(co) == 'dead' do +-- t[k] = nil + -- See coro.yield(): sometimes we traverse a copy of _coros, but if we + -- discover a dead coroutine in that copy, delete it from _coros + -- anyway. Deleting it from a temporary copy does nothing. + coro._coros[k] = nil + coroutine.close(co) + k, co = next(t, k) + end + return co +end + +return coro diff --git a/indra/newview/scripts/lua/require/fiber.lua b/indra/newview/scripts/lua/require/fiber.lua new file mode 100644 index 0000000000..cae27b936b --- /dev/null +++ b/indra/newview/scripts/lua/require/fiber.lua @@ -0,0 +1,340 @@ +-- Organize Lua coroutines into fibers. + +-- In this usage, the difference between coroutines and fibers is that fibers +-- have a scheduler. Yielding a fiber means allowing other fibers, plural, to +-- run: it's more than just returning control to the specific Lua thread that +-- resumed the running coroutine. + +-- fiber.launch() creates a new fiber ready to run. +-- fiber.status() reports (augmented) status of the passed fiber: instead of +-- 'suspended', it returns either 'ready' or 'waiting' +-- fiber.yield() allows other fibers to run, but leaves the calling fiber +-- ready to run. +-- fiber.wait() marks the running fiber not ready, and resumes other fibers. +-- fiber.wake() marks the designated suspended fiber ready to run, but does +-- not yet resume it. +-- fiber.run() runs all current fibers until all have terminated (successfully +-- or with an error). + +local printf = require 'printf' +local function dbg(...) end +-- local dbg = printf +local coro = require 'coro' + +local fiber = {} + +-- The tables in which we track fibers must have weak keys so dead fibers +-- can be garbage-collected. +local weak_values = {__mode='v'} +local weak_keys = {__mode='k'} + +-- Track each current fiber as being either ready to run or not ready +-- (waiting). wait() moves the running fiber from ready to waiting; wake() +-- moves the designated fiber from waiting back to ready. +-- The ready table is used as a list so yield() can go round robin. +local ready = setmetatable({'main'}, weak_keys) +-- The waiting table is used as a set because order doesn't matter. +local waiting = setmetatable({}, weak_keys) + +-- Every fiber has a name, for diagnostic purposes. Names must be unique. +-- A colliding name will be suffixed with an integer. +-- Predefine 'main' with our marker so nobody else claims that name. +local names = setmetatable({main='main'}, weak_keys) +local byname = setmetatable({main='main'}, weak_values) +-- each colliding name has its own distinct suffix counter +local suffix = {} + +-- Specify a nullary idle() callback to be called whenever there are no ready +-- fibers but there are waiting fibers. The idle() callback is responsible for +-- changing zero or more waiting fibers to ready fibers by calling +-- fiber.wake(), although a given call may leave them all still waiting. +-- When there are no ready fibers, it's a good idea for the idle() function to +-- return control to a higher-level execution agent. Simply returning without +-- changing any fiber's status will spin the CPU. +-- The idle() callback can return non-nil to exit fiber.run() with that value. +function fiber._idle() + error('fiber.yield(): you must first call set_idle(nullary idle() function)') +end + +function fiber.set_idle(func) + fiber._idle = func +end + +-- Launch a new Lua fiber, ready to run. +function fiber.launch(name, func, ...) + local args = table.pack(...) + local co = coroutine.create(function() func(table.unpack(args)) end) + -- a new fiber is ready to run + table.insert(ready, co) + local namekey = name + while byname[namekey] do + if not suffix[name] then + suffix[name] = 1 + end + suffix[name] += 1 + namekey = name .. tostring(suffix[name]) + end + -- found a namekey not yet in byname: set it + byname[namekey] = co + -- and remember it as this fiber's name + names[co] = namekey +-- dbg('launch(%s)', namekey) +-- dbg('byname[%s] = %s', namekey, tostring(byname[namekey])) +-- dbg('names[%s] = %s', tostring(co), names[co]) +-- dbg('ready[-1] = %s', tostring(ready[#ready])) +end + +-- for debugging +function format_all() + output = {} + table.insert(output, 'Ready fibers:' .. if next(ready) then '' else ' none') + for _, co in pairs(ready) do + table.insert(output, string.format(' %s: %s', fiber.get_name(co), fiber.status(co))) + end + table.insert(output, 'Waiting fibers:' .. if next(waiting) then '' else ' none') + for co in pairs(waiting) do + table.insert(output, string.format(' %s: %s', fiber.get_name(co), fiber.status(co))) + end + return table.concat(output, '\n') +end + +function fiber.print_all() + print(format_all()) +end + +-- return either the running coroutine or, if called from the main thread, +-- 'main' +function fiber.running() + return coroutine.running() or 'main' +end + +-- Query a fiber's name (nil for the running fiber) +function fiber.get_name(co) + return names[co or fiber.running()] or 'unknown' +end + +-- Query status of the passed fiber +function fiber.status(co) + local running = coroutine.running() + if (not co) or co == running then + -- silly to ask the status of the running fiber: it's 'running' + return 'running' + end + if co ~= 'main' then + -- for any coroutine but main, consult coroutine.status() + local status = coroutine.status(co) + if status ~= 'suspended' then + return status + end + -- here co is suspended, answer needs further refinement + else + -- co == 'main' + if not running then + -- asking about 'main' from the main fiber + return 'running' + end + -- asking about 'main' from some other fiber, so presumably main is suspended + end + -- here we know co is suspended -- but is it ready to run? + if waiting[co] then + return 'waiting' + end + -- not waiting should imply ready: sanity check + if table.find(ready, co) then + return 'ready' + end + -- Calls within yield() between popping the next ready fiber and + -- re-appending it to the list are in this state. Once we're done + -- debugging yield(), we could reinstate either of the below. +-- error(string.format('fiber.status(%s) is stumped', fiber.get_name(co))) +-- print(string.format('*** fiber.status(%s) is stumped', fiber.get_name(co))) + return '(unknown)' +end + +-- change the running fiber's status to waiting +local function set_waiting() + -- if called from the main fiber, inject a 'main' marker into the list + co = fiber.running() + -- delete from ready list + local i = table.find(ready, co) + if i then + table.remove(ready, i) + end + -- add to waiting list + waiting[co] = true +end + +-- Suspend the current fiber until some other fiber calls fiber.wake() on it +function fiber.wait() + dbg('Fiber %q waiting', fiber.get_name()) + set_waiting() + -- now yield to other fibers + fiber.yield() +end + +-- Mark a suspended fiber as being ready to run +function fiber.wake(co) + if not waiting[co] then + error(string.format('fiber.wake(%s) but status=%s, ready=%s, waiting=%s', + names[co], fiber.status(co), ready[co], waiting[co])) + end + -- delete from waiting list + waiting[co] = nil + -- add to end of ready list + table.insert(ready, co) + dbg('Fiber %q ready', fiber.get_name(co)) + -- but don't yet resume it: that happens next time we reach yield() +end + +-- pop and return the next not-dead fiber in the ready list, or nil if none remain +local function live_ready_iter() + -- don't write: + -- for co in table.remove, ready, 1 + -- because it would keep passing a new second parameter! + for co in function() return table.remove(ready, 1) end do + dbg('%s live_ready_iter() sees %s, status %s', + fiber.get_name(), fiber.get_name(co), fiber.status(co)) + -- keep removing the head entry until we find one that's not dead, + -- discarding any dead coroutines along the way + if co == 'main' or coroutine.status(co) ~= 'dead' then + dbg('%s live_ready_iter() returning %s', + fiber.get_name(), fiber.get_name(co)) + return co + end + end + dbg('%s live_ready_iter() returning nil', fiber.get_name()) + return nil +end + +-- prune the set of waiting fibers +local function prune_waiting() + for waiter in pairs(waiting) do + if waiter ~= 'main' and coroutine.status(waiter) == 'dead' then + waiting[waiter] = nil + end + end +end + +-- Run other ready fibers, leaving this one ready, returning after a cycle. +-- Returns: +-- * true, nil if there remain other live fibers, whether ready or waiting, +-- but it's our turn to run +-- * false, nil if this is the only remaining fiber +-- * nil, x if configured idle() callback returns non-nil x +local function scheduler() + dbg('scheduler():\n%s', format_all()) + -- scheduler() is asymmetric because Lua distinguishes the main thread + -- from other coroutines. The main thread can't yield; it can only resume + -- other coroutines. So although an arbitrary coroutine could resume still + -- other arbitrary coroutines, it could NOT resume the main thread because + -- the main thread can't yield. Therefore, scheduler() delegates its real + -- processing to the main thread. If called from a coroutine, pass control + -- back to the main thread. + if coroutine.running() then + -- this is a real coroutine, yield normally to main thread + coroutine.yield() + -- main certainly still exists + return true + end + + -- This is the main fiber: coroutine.yield() doesn't work. + -- Instead, resume each of the ready fibers. + -- Prune the set of waiting fibers after every time fiber business logic + -- runs (i.e. other fibers might have terminated or hit error), such as + -- here on entry. + prune_waiting() + local others, idle_stop + repeat + for co in live_ready_iter do + -- seize the opportunity to make sure the viewer isn't shutting down + LL.check_stop() + -- before we re-append co, is it the only remaining entry? + others = next(ready) + -- co is live, re-append it to the ready list + table.insert(ready, co) + if co == 'main' then + -- Since we know the caller is the main fiber, it's our turn. + -- Tell caller if there are other ready or waiting fibers. + return others or next(waiting) + end + -- not main, but some other ready coroutine: + -- use coro.resume() so we'll propagate any error encountered + coro.resume(co) + prune_waiting() + end + -- Here there are no ready fibers. Are there any waiting fibers? + if not next(waiting) then + return false + end + -- there are waiting fibers: call consumer's configured idle() function + idle_stop = fiber._idle() + if idle_stop ~= nil then + return nil, idle_stop + end + prune_waiting() + -- loop "forever", that is, until: + -- * main is ready, or + -- * there are neither ready fibers nor waiting fibers, or + -- * fiber._idle() returned non-nil + until false +end + +-- Let other fibers run. This is useful in either of two cases: +-- * fiber.wait() calls this to run other fibers while this one is waiting. +-- fiber.yield() (and therefore fiber.wait()) works from the main thread as +-- well as from explicitly-launched fibers, without the caller having to +-- care. +-- * A long-running fiber that doesn't often call fiber.wait() should sprinkle +-- in fiber.yield() calls to interleave processing on other fibers. +function fiber.yield() + -- The difference between this and fiber.run() is that fiber.yield() + -- assumes its caller has work to do. yield() returns to its caller as + -- soon as scheduler() pops this fiber from the ready list. fiber.run() + -- continues looping until all other fibers have terminated, or the + -- set_idle() callback tells it to stop. + local others, idle_done = scheduler() + -- scheduler() returns either if we're ready, or if idle_done ~= nil. + if idle_done ~= nil then + -- Returning normally from yield() means the caller can carry on with + -- its pending work. But in this case scheduler() returned because the + -- configured set_idle() function interrupted it -- not because we're + -- actually ready. Don't return normally. + error('fiber.set_idle() interrupted yield() with: ' .. tostring(idle_done)) + end + -- We're ready! Just return to caller. In this situation we don't care + -- whether there are other ready fibers. + dbg('fiber.yield() returning to %s (%sothers are ready)', + fiber.get_name(), ((not others) and "no " or "")) +end + +-- Run fibers until all but main have terminated: return nil. +-- Or until configured idle() callback returns x ~= nil: return x. +function fiber.run() + -- A fiber calling run() is not also doing other useful work. Remove the + -- calling fiber from the ready list. Otherwise yield() would keep seeing + -- that our caller is ready and return to us, instead of realizing that + -- all coroutines are waiting and call idle(). But don't say we're + -- waiting, either, because then when all other fibers have terminated + -- we'd call idle() forever waiting for something to make us ready again. + local i = table.find(ready, fiber.running()) + if i then + table.remove(ready, i) + end + local others, idle_done + repeat + dbg('%s calling fiber.run() calling scheduler()', fiber.get_name()) + others, idle_done = scheduler() + dbg("%s fiber.run()'s scheduler() returned %s, %s", fiber.get_name(), + tostring(others), tostring(idle_done)) + until (not others) + dbg('%s fiber.run() done', fiber.get_name()) + -- For whatever it's worth, put our own fiber back in the ready list. + table.insert(ready, fiber.running()) + -- Once there are no more waiting fibers, and the only ready fiber is + -- us, return to caller. All previously-launched fibers are done. Possibly + -- the chunk is done, or the chunk may decide to launch a new batch of + -- fibers. + return idle_done +end + +return fiber diff --git a/indra/newview/scripts/lua/require/inspect.lua b/indra/newview/scripts/lua/require/inspect.lua new file mode 100644 index 0000000000..9900a0b81b --- /dev/null +++ b/indra/newview/scripts/lua/require/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, ' = ') + 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/require/leap.lua b/indra/newview/scripts/lua/require/leap.lua new file mode 100644 index 0000000000..82f91ce9e9 --- /dev/null +++ b/indra/newview/scripts/lua/require/leap.lua @@ -0,0 +1,550 @@ +-- 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. +-- +-- Usage: +-- 1. Launch some number of Lua coroutines. The code in each coroutine may +-- call leap.send(), leap.request() or leap.generate(). leap.send() returns +-- immediately ("fire and forget"). leap.request() blocks the calling +-- coroutine until it receives and returns the viewer's response to its +-- request. leap.generate() expects an arbitrary number of responses to the +-- original request. +-- 2. To handle events from the viewer other than direct responses to +-- requests, instantiate a leap.WaitFor object with a filter(pump, data) +-- override method that returns non-nil for desired events. A coroutine may +-- call wait() on any such WaitFor. +-- 3. Once the coroutines have been launched, call leap.process() on the main +-- coroutine. process() retrieves incoming events from the viewer and +-- dispatches them to waiting request() or generate() calls, or to +-- appropriate WaitFor instances. process() returns when either +-- get_event_next() raises an error or the viewer posts nil to the script's +-- reply pump to indicate it's done. +-- 4. Alternatively, a running coroutine may call leap.done() to break out of +-- leap.process(). process() won't notice until the next event from the +-- viewer, though. + +local fiber = require('fiber') +local ErrorQueue = require('ErrorQueue') +local inspect = require('inspect') +local function dbg(...) end +-- local dbg = require('printf') +local util = require('util') + +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. +local reply, command = LL.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 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. +-- pending is NOT a weak table because the caller of request() or generate() +-- never sees the WaitForReqid object. pending holds the only reference, so +-- it should NOT be garbage-collected. +local 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. +-- Anyone who instantiates a WaitFor subclass object should retain a reference +-- to it. Once the consuming script drops the reference, allow Lua to +-- garbage-collect the WaitFor despite its entry in waitfors. +local weak_values = {__mode='v'} +local waitfors = setmetatable({}, weak_values) +-- 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 +-- break leap.process() loop +leap._done = false + +-- get the name of the reply pump +function leap.replypump() + return reply +end + +-- get the name of the command pump +function leap.cmdpump() + return 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'] = reply + if reqid ~= nil then + data['reqid'] = reqid + end + end + dbg('leap.send(%s, %s) calling post_on()', pump, data) + LL.post_on(pump, data) +end + +-- common setup code shared by request() and generate() +local function requestSetup(pump, data) + -- invent a new, unique reqid + leap._reqid += 1 + local reqid = leap._reqid + -- Instantiate a new WaitForReqid object. The priority is irrelevant + -- because, unlike the WaitFor base class, WaitForReqid does not + -- self-register on our waitfors list. Instead, capture the new + -- WaitForReqid object in pending so dispatch() can find it. + local waitfor = leap.WaitForReqid(reqid) + pending[reqid] = waitfor + -- Pass reqid to send() to stamp it into (a copy of) the request data. + dbg('requestSetup(%s, %s) storing %s', pump, data, waitfor.name) + leap.send(pump, data, reqid) + return reqid, waitfor +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. +-- +-- See also send(), generate(). +function leap.request(pump, data) + local reqid, waitfor = requestSetup(pump, data) + dbg('leap.request(%s, %s) about to wait on %s', pump, data, tostring(waitfor)) + local ok, response = pcall(waitfor.wait, waitfor) + dbg('leap.request(%s, %s) got %s: %s', pump, data, ok, response) + -- kill off temporary WaitForReqid object, even if error + pending[reqid] = nil + if not ok then + error(response) + elseif response.error then + error(response.error) + else + return response + end +end + +-- Send the specified request LLSD, expecting an arbitrary number of replies. +-- Each one is returned on request. +-- +-- Usage: +-- sequence = leap.generate(pump, data) +-- repeat +-- response = sequence.next() +-- until last(response) +-- (last() means whatever test the caller wants to perform on response) +-- sequence.done() +-- +-- See request() remarks about ["reqid"]. +-- +-- Note: this seems like a prime use case for Lua coroutines. But in a script +-- using fibers.lua, a "wild" coroutine confuses the fiber scheduler. If +-- generate() were itself a coroutine, it would call WaitForReqid:wait(), +-- which would yield -- thereby resuming generate() WITHOUT waiting. +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, waitfor = requestSetup(pump, data) + return { + next = function() + dbg('leap.generate(%s).next() about to wait on %s', reqid, tostring(waitfor)) + local ok, response = pcall(waitfor.wait, waitfor) + dbg('leap.generate(%s).next() got %s: %s', reqid, ok, response) + if not ok then + error(response) + elseif response.error then + error(response.error) + else + return response + end + end, + done = function() + -- cleanup consists of removing our WaitForReqid from pending + pending[reqid] = nil + end + } +end + +-- Send the specified request LLSD, expecting an immediate reply followed by +-- an arbitrary number of subsequent replies with the same reqid. Block the +-- calling coroutine until the first (immediate) reply, but launch a separate +-- fiber on which to call the passed callback with later replies. +-- +-- Once the callback returns true, the background fiber terminates. +function leap.eventstream(pump, data, callback) + local reqid, waitfor = requestSetup(pump, data) + local response = waitfor:wait() + if response.error then + -- clean up our WaitForReqid + waitfor:close() + error(response.error) + end + -- No error, so far so good: + -- call the callback with the first response just in case + dbg('leap.eventstream(%s): first callback', reqid) + local ok, done = pcall(callback, response) + dbg('leap.eventstream(%s) got %s, %s', reqid, ok, done) + if not ok then + -- clean up our WaitForReqid + waitfor:close() + error(done) + end + if done then + return response + end + -- callback didn't throw an error, and didn't say stop, + -- so set up to handle subsequent events + -- TODO: distinguish "daemon" fibers that can be terminated even if waiting + fiber.launch( + pump, + function () + local ok, done + local nth = 1 + repeat + event = waitfor:wait() + if not event then + -- wait() returns nil once the queue is closed (e.g. cancelreq()) + ok, done = true, true + else + nth += 1 + dbg('leap.eventstream(%s): callback %d', reqid, nth) + ok, done = pcall(callback, event) + dbg('leap.eventstream(%s) got %s, %s', reqid, ok, done) + end + -- not ok means callback threw an error (caught as 'done') + -- done means callback succeeded but wants to stop + until (not ok) or done + -- once we break this loop, clean up our WaitForReqid + waitfor:close() + if not ok then + -- can't reflect the error back to our caller + LL.print_warning(fiber.get_name() .. ': ' .. done) + end + end) + return response +end + +-- we might want to clean up after leap.eventstream() even if the callback has +-- not yet returned true +function leap.cancelreq(reqid) + dbg('cancelreq(%s)', reqid) + local waitfor = pending[reqid] + if waitfor ~= nil then + -- close() removes the pending entry and also closes the queue, + -- breaking the background fiber's wait loop. + dbg('cancelreq(%s) canceling %s', reqid, waitfor.name) + waitfor:close() + end +end + +local function cleanup(message) + -- We're done: clean up all pending coroutines. + -- Iterate over copies of the pending and waitfors tables, since the + -- close() operation modifies the real tables. + for i, waitfor in pairs(table.clone(pending)) do + waitfor:close() + end + for i, waitfor in pairs(table.clone(waitfors)) do + waitfor:close() + end +end + +-- Handle an incoming (pump, data) event with no recognizable ['reqid'] +local function 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(waitfors) do + dbg('unsolicited() checking %s', waitfor.name) + if waitfor:handle(pump, data) then + return + end + end + LL.print_debug(string.format('unsolicited(%s, %s) discarding unclaimed event', + pump, inspect(data))) +end + +-- Route incoming (pump, data) event to the appropriate waiting coroutine. +local function dispatch(pump, data) + local reqid = data['reqid'] + -- if the response has no 'reqid', it's not from request() or generate() + if reqid == nil then +-- dbg('dispatch() found no reqid; calling unsolicited(%s, %s)', pump, data) + return unsolicited(pump, data) + end + -- have reqid; do we have a WaitForReqid? + local waitfor = pending[reqid] + if waitfor == nil then +-- dbg('dispatch() found no WaitForReqid(%s); calling unsolicited(%s, %s)', reqid, pump, data) + return unsolicited(pump, data) + end + -- found the right WaitForReqid object, let it handle the event +-- dbg('dispatch() calling %s.handle(%s, %s)', waitfor.name, pump, data) + waitfor:handle(pump, data) +end + +-- We configure fiber.set_idle() function. fiber.yield() calls the configured +-- idle callback whenever there are waiting fibers but no ready fibers. In +-- our case, that means it's time to fetch another incoming viewer event. +fiber.set_idle(function () + -- If someone has called leap.done(), then tell fiber.yield() to break loop. + if leap._done then + cleanup('done') + return 'done' + end + dbg('leap.idle() calling get_event_next()') + local ok, pump, data = pcall(LL.get_event_next) + dbg('leap.idle() got %s: %s, %s', ok, pump, data) + -- ok false means get_event_next() raised a Lua error, pump is message + if not ok then + cleanup(pump) + error(pump) + end + -- data nil means get_event_next() returned (pump, LLSD()) to indicate done + if not data then + cleanup('end') + return 'end' + end + -- got a real pump, data pair + dispatch(pump, data) + -- return to fiber.yield(): any incoming message might result in one or + -- more fibers becoming ready +end) + +function leap.done() + leap._done = true +end + +-- called by WaitFor.enable() +local function registerWaitFor(waitfor) + table.insert(waitfors, waitfor) + -- keep waitfors sorted in descending order of specified priority + table.sort(waitfors, + function (lhs, rhs) return lhs.priority > rhs.priority end) +end + +-- called by WaitFor.disable() +local function unregisterWaitFor(waitfor) + local i = table.find(waitfors, waitfor) + if i ~= nil then + waitfors[i] = nil + 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.tostring(self) + -- Lua (sub)classes have no name; can't prefix with that + return self.name +end + +function leap.WaitFor:new(priority, name) + local obj = setmetatable({__tostring=leap.WaitFor.tostring}, 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() + obj._registered = false + -- if no priority, then don't enable() - remember 0 is truthy + if priority then + obj:enable() + end + + return obj +end + +util.classctor(leap.WaitFor) + +-- Re-enable a disable()d WaitFor object. New WaitFor objects are +-- enable()d by default. +function leap.WaitFor:enable() + if not self._registered then + registerWaitFor(self) + self._registered = true + end +end + +-- Disable an enable()d WaitFor object. +function leap.WaitFor:disable() + if self._registered then + 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() + dbg('%s about to wait', self.name) + local item = self._queue:Dequeue() + dbg('%s got %s', self.name, item) + return item +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 override the WaitFor.filter() method') +end + +-- called by unsolicited() for each WaitFor in waitfors +function leap.WaitFor:handle(pump, data) + local item = self:filter(pump, data) + dbg('%s.filter() returned %s', self.name, item) + -- 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 cleanup() at end +function leap.WaitFor:close() + self:disable() + self._queue:close() +end + +-- called by leap.process() when get_event_next() raises an error +function leap.WaitFor:exception(message) + LL.print_warning(self.name .. ' error: ' .. message) + self._queue:Error(message) +end + +-- ------------------------------ WaitForReqid ------------------------------- +leap.WaitForReqid = leap.WaitFor() + +function leap.WaitForReqid:new(reqid) + -- priority is meaningless, since this object won't be added to the + -- priority-sorted waitfors list. Use the reqid as the debugging name + -- string. + local obj = leap.WaitFor(nil, 'WaitForReqid(' .. reqid .. ')') + setmetatable(obj, self) + self.__index = self + + obj.reqid = reqid + + return obj +end + +util.classctor(leap.WaitForReqid) + +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 + +function leap.WaitForReqid:close() + -- remove this entry from pending table + pending[self.reqid] = nil + self._queue:close() +end + +return leap diff --git a/indra/newview/scripts/lua/require/printf.lua b/indra/newview/scripts/lua/require/printf.lua new file mode 100644 index 0000000000..e84b2024df --- /dev/null +++ b/indra/newview/scripts/lua/require/printf.lua @@ -0,0 +1,19 @@ +-- printf(...) is short for print(string.format(...)) + +local inspect = require 'inspect' + +local function printf(format, ...) + -- string.format() only handles numbers and strings. + -- Convert anything else to string using the inspect module. + local args = {} + for _, arg in pairs(table.pack(...)) do + if type(arg) == 'number' or type(arg) == 'string' then + table.insert(args, arg) + else + table.insert(args, inspect(arg)) + end + end + print(string.format(format, table.unpack(args))) +end + +return printf diff --git a/indra/newview/scripts/lua/require/startup.lua b/indra/newview/scripts/lua/require/startup.lua new file mode 100644 index 0000000000..c3040f94b8 --- /dev/null +++ b/indra/newview/scripts/lua/require/startup.lua @@ -0,0 +1,100 @@ +-- query, wait for or mandate a particular viewer startup state + +-- During startup, the viewer steps through a sequence of numbered (and named) +-- states. This can be used to detect when, for instance, the login screen is +-- displayed, or when the viewer has finished logging in and is fully +-- in-world. + +local fiber = require 'fiber' +local leap = require 'leap' +local inspect = require 'inspect' +local function dbg(...) end +-- local dbg = require 'printf' + +-- --------------------------------------------------------------------------- +local startup = {} + +-- Get the list of startup states from the viewer. +local bynum = leap.request('LLStartUp', {op='getStateTable'})['table'] + +local byname = setmetatable( + {}, + -- set metatable to throw an error if you look up invalid state name + {__index=function(t, k) + local v = rawget(t, k) + if v then + return v + end + error(string.format('startup module passed invalid state %q', k), 2) + end}) + +-- derive byname as a lookup table to find the 0-based index for a given name +for i, name in pairs(bynum) do + -- the viewer's states are 0-based, not 1-based like Lua indexes + byname[name] = i - 1 +end +-- dbg('startup states: %s', inspect(byname)) + +-- specialize a WaitFor to track the viewer's startup state +local startup_pump = 'StartupState' +local waitfor = leap.WaitFor(0, startup_pump) +function waitfor:filter(pump, data) + if pump == self.name then + return data + end +end + +function waitfor:process(data) + -- keep updating startup._state for interested parties + startup._state = data.str + dbg('startup updating state to %q', data.str) + -- now pass data along to base-class method to queue + leap.WaitFor.process(self, data) +end + +-- listen for StartupState events +leap.request(leap.cmdpump(), + {op='listen', source=startup_pump, listener='startup.lua', tweak=true}) +-- poke LLStartUp to make sure we get an event +leap.send('LLStartUp', {op='postStartupState'}) + +-- --------------------------------------------------------------------------- +-- wait for response from postStartupState +while not startup._state do + dbg('startup.state() waiting for first StartupState event') + waitfor:wait() +end + +-- return a list of all known startup states +function startup.list() + return bynum +end + +-- report whether state with string name 'left' is before string name 'right' +function startup.before(left, right) + return byname[left] < byname[right] +end + +-- report the viewer's current startup state +function startup.state() + return startup._state +end + +-- error if script is called before specified state string name +function startup.ensure(state) + if startup.before(startup.state(), state) then + -- tell error() to pretend this error was thrown by our caller + error('must not be called before startup state ' .. state, 2) + end +end + +-- block calling fiber until viewer has reached state with specified string name +function startup.wait(state) + dbg('startup.wait(%q)', state) + while startup.before(startup.state(), state) do + local item = waitfor:wait() + dbg('startup.wait(%q) sees %s', state, item) + end +end + +return startup diff --git a/indra/newview/scripts/lua/require/timers.lua b/indra/newview/scripts/lua/require/timers.lua new file mode 100644 index 0000000000..e4938078dc --- /dev/null +++ b/indra/newview/scripts/lua/require/timers.lua @@ -0,0 +1,104 @@ +-- Access to the viewer's time-delay facilities + +local leap = require 'leap' +local util = require 'util' + +local timers = {} + +local function dbg(...) end +-- local dbg = require 'printf' + +timers.Timer = {} + +-- delay: time in seconds until callback +-- callback: 'wait', or function to call when timer fires (self:tick if nil) +-- iterate: if non-nil, call callback repeatedly until it returns non-nil +-- (ignored if 'wait') +function timers.Timer:new(delay, callback, iterate) + local obj = setmetatable({}, self) + self.__index = self + + if callback == 'wait' then + dbg('scheduleAfter(%d):', delay) + sequence = leap.generate('Timers', {op='scheduleAfter', after=delay}) + -- ignore the immediate return + dbg('scheduleAfter(%d) -> %s', delay, + sequence.next()) + -- this call is where we wait for real + dbg('next():') + dbg('next() -> %s', + sequence.next()) + sequence.done() + return + end + + callback = callback or function() obj:tick() end + + local first = true + if iterate then + obj.id = leap.eventstream( + 'Timers', + {op='scheduleEvery', every=delay}, + function (event) + local reqid = event.reqid + if first then + first = false + dbg('timer(%s) first callback', reqid) + -- discard the first (immediate) response: don't call callback + return nil + else + dbg('timer(%s) nth callback', reqid) + return callback(event) + end + end + ).reqid + else + obj.id = leap.eventstream( + 'Timers', + {op='scheduleAfter', after=delay}, + function (event) + -- Arrange to return nil the first time, true the second. This + -- callback is called immediately with the response to + -- 'scheduleAfter', and if we immediately returned true, we'd + -- be done, and the subsequent timer event would be discarded. + if first then + first = false + -- Caller doesn't expect an immediate callback. + return nil + else + callback(event) + -- Since caller doesn't want to iterate, the value + -- returned by the callback is irrelevant: just stop after + -- this one and only call. + return true + end + end + ).reqid + end + + return obj +end + +util.classctor(timers.Timer) + +function timers.Timer:tick() + error('Pass a callback to Timer:new(), or override Timer:tick()') +end + +function timers.Timer:cancel() + local ok = leap.request('Timers', {op='cancel', id=self.id}).ok + leap.cancelreq(self.id) + return ok +end + +function timers.Timer:isRunning() + return leap.request('Timers', {op='isRunning', id=self.id}).running +end + +-- returns (true, seconds left) for a live timer, else (false, 0) +function timers.Timer:timeUntilCall() + local result = leap.request('Timers', {op='timeUntilCall', id=self.id}) + return result.ok, result.remaining +end + +return timers diff --git a/indra/newview/scripts/lua/require/util.lua b/indra/newview/scripts/lua/require/util.lua new file mode 100644 index 0000000000..bfbfc8637c --- /dev/null +++ b/indra/newview/scripts/lua/require/util.lua @@ -0,0 +1,69 @@ +-- utility functions, in alpha order + +local util = {} + +-- Allow MyClass(ctor args...) equivalent to MyClass:new(ctor args...) +-- Usage: +-- local MyClass = {} +-- function MyClass:new(...) +-- ... +-- end +-- ... +-- util.classctor(MyClass) +-- or if your constructor is named something other than MyClass:new(), e.g. +-- MyClass:construct(): +-- util.classctor(MyClass, MyClass.construct) +-- return MyClass +function util.classctor(class, ctor) + -- get the metatable for the passed class + local mt = getmetatable(class) + if mt == nil then + -- if it doesn't already have a metatable, then create one + mt = {} + setmetatable(class, mt) + end + -- now that class has a metatable, set its __call method to the specified + -- constructor method (class.new if not specified) + mt.__call = ctor or class.new +end + +-- check if array-like table contains certain value +function util.contains(t, v) + return table.find(t, v) ~= nil +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 + +-- cheap test whether table t is empty +function util.empty(t) + return not next(t) +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 + +return util diff --git a/indra/newview/scripts/lua/startup.lua b/indra/newview/scripts/lua/startup.lua deleted file mode 100644 index c3040f94b8..0000000000 --- a/indra/newview/scripts/lua/startup.lua +++ /dev/null @@ -1,100 +0,0 @@ --- query, wait for or mandate a particular viewer startup state - --- During startup, the viewer steps through a sequence of numbered (and named) --- states. This can be used to detect when, for instance, the login screen is --- displayed, or when the viewer has finished logging in and is fully --- in-world. - -local fiber = require 'fiber' -local leap = require 'leap' -local inspect = require 'inspect' -local function dbg(...) end --- local dbg = require 'printf' - --- --------------------------------------------------------------------------- -local startup = {} - --- Get the list of startup states from the viewer. -local bynum = leap.request('LLStartUp', {op='getStateTable'})['table'] - -local byname = setmetatable( - {}, - -- set metatable to throw an error if you look up invalid state name - {__index=function(t, k) - local v = rawget(t, k) - if v then - return v - end - error(string.format('startup module passed invalid state %q', k), 2) - end}) - --- derive byname as a lookup table to find the 0-based index for a given name -for i, name in pairs(bynum) do - -- the viewer's states are 0-based, not 1-based like Lua indexes - byname[name] = i - 1 -end --- dbg('startup states: %s', inspect(byname)) - --- specialize a WaitFor to track the viewer's startup state -local startup_pump = 'StartupState' -local waitfor = leap.WaitFor(0, startup_pump) -function waitfor:filter(pump, data) - if pump == self.name then - return data - end -end - -function waitfor:process(data) - -- keep updating startup._state for interested parties - startup._state = data.str - dbg('startup updating state to %q', data.str) - -- now pass data along to base-class method to queue - leap.WaitFor.process(self, data) -end - --- listen for StartupState events -leap.request(leap.cmdpump(), - {op='listen', source=startup_pump, listener='startup.lua', tweak=true}) --- poke LLStartUp to make sure we get an event -leap.send('LLStartUp', {op='postStartupState'}) - --- --------------------------------------------------------------------------- --- wait for response from postStartupState -while not startup._state do - dbg('startup.state() waiting for first StartupState event') - waitfor:wait() -end - --- return a list of all known startup states -function startup.list() - return bynum -end - --- report whether state with string name 'left' is before string name 'right' -function startup.before(left, right) - return byname[left] < byname[right] -end - --- report the viewer's current startup state -function startup.state() - return startup._state -end - --- error if script is called before specified state string name -function startup.ensure(state) - if startup.before(startup.state(), state) then - -- tell error() to pretend this error was thrown by our caller - error('must not be called before startup state ' .. state, 2) - end -end - --- block calling fiber until viewer has reached state with specified string name -function startup.wait(state) - dbg('startup.wait(%q)', state) - while startup.before(startup.state(), state) do - local item = waitfor:wait() - dbg('startup.wait(%q) sees %s', state, item) - end -end - -return startup diff --git a/indra/newview/scripts/lua/timers.lua b/indra/newview/scripts/lua/timers.lua deleted file mode 100644 index e4938078dc..0000000000 --- a/indra/newview/scripts/lua/timers.lua +++ /dev/null @@ -1,104 +0,0 @@ --- Access to the viewer's time-delay facilities - -local leap = require 'leap' -local util = require 'util' - -local timers = {} - -local function dbg(...) end --- local dbg = require 'printf' - -timers.Timer = {} - --- delay: time in seconds until callback --- callback: 'wait', or function to call when timer fires (self:tick if nil) --- iterate: if non-nil, call callback repeatedly until it returns non-nil --- (ignored if 'wait') -function timers.Timer:new(delay, callback, iterate) - local obj = setmetatable({}, self) - self.__index = self - - if callback == 'wait' then - dbg('scheduleAfter(%d):', delay) - sequence = leap.generate('Timers', {op='scheduleAfter', after=delay}) - -- ignore the immediate return - dbg('scheduleAfter(%d) -> %s', delay, - sequence.next()) - -- this call is where we wait for real - dbg('next():') - dbg('next() -> %s', - sequence.next()) - sequence.done() - return - end - - callback = callback or function() obj:tick() end - - local first = true - if iterate then - obj.id = leap.eventstream( - 'Timers', - {op='scheduleEvery', every=delay}, - function (event) - local reqid = event.reqid - if first then - first = false - dbg('timer(%s) first callback', reqid) - -- discard the first (immediate) response: don't call callback - return nil - else - dbg('timer(%s) nth callback', reqid) - return callback(event) - end - end - ).reqid - else - obj.id = leap.eventstream( - 'Timers', - {op='scheduleAfter', after=delay}, - function (event) - -- Arrange to return nil the first time, true the second. This - -- callback is called immediately with the response to - -- 'scheduleAfter', and if we immediately returned true, we'd - -- be done, and the subsequent timer event would be discarded. - if first then - first = false - -- Caller doesn't expect an immediate callback. - return nil - else - callback(event) - -- Since caller doesn't want to iterate, the value - -- returned by the callback is irrelevant: just stop after - -- this one and only call. - return true - end - end - ).reqid - end - - return obj -end - -util.classctor(timers.Timer) - -function timers.Timer:tick() - error('Pass a callback to Timer:new(), or override Timer:tick()') -end - -function timers.Timer:cancel() - local ok = leap.request('Timers', {op='cancel', id=self.id}).ok - leap.cancelreq(self.id) - return ok -end - -function timers.Timer:isRunning() - return leap.request('Timers', {op='isRunning', id=self.id}).running -end - --- returns (true, seconds left) for a live timer, else (false, 0) -function timers.Timer:timeUntilCall() - local result = leap.request('Timers', {op='timeUntilCall', id=self.id}) - return result.ok, result.remaining -end - -return timers diff --git a/indra/newview/scripts/lua/util.lua b/indra/newview/scripts/lua/util.lua deleted file mode 100644 index bfbfc8637c..0000000000 --- a/indra/newview/scripts/lua/util.lua +++ /dev/null @@ -1,69 +0,0 @@ --- utility functions, in alpha order - -local util = {} - --- Allow MyClass(ctor args...) equivalent to MyClass:new(ctor args...) --- Usage: --- local MyClass = {} --- function MyClass:new(...) --- ... --- end --- ... --- util.classctor(MyClass) --- or if your constructor is named something other than MyClass:new(), e.g. --- MyClass:construct(): --- util.classctor(MyClass, MyClass.construct) --- return MyClass -function util.classctor(class, ctor) - -- get the metatable for the passed class - local mt = getmetatable(class) - if mt == nil then - -- if it doesn't already have a metatable, then create one - mt = {} - setmetatable(class, mt) - end - -- now that class has a metatable, set its __call method to the specified - -- constructor method (class.new if not specified) - mt.__call = ctor or class.new -end - --- check if array-like table contains certain value -function util.contains(t, v) - return table.find(t, v) ~= nil -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 - --- cheap test whether table t is empty -function util.empty(t) - return not next(t) -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 - -return util diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py index 53039fbd99..20dced2341 100755 --- a/indra/newview/viewer_manifest.py +++ b/indra/newview/viewer_manifest.py @@ -169,6 +169,8 @@ class ViewerManifest(LLManifest): with self.prefix(src_dst="scripts/lua"): self.path("*.lua") self.path("*.xml") + with self.prefix(src_dst='require'): + self.path("*.lua") #build_data.json. Standard with exception handling is fine. If we can't open a new file for writing, we have worse problems #platform is computed above with other arg parsing @@ -285,7 +287,7 @@ class ViewerManifest(LLManifest): # A line that starts with a non-whitespace character is a name; all others describe contributions, so collect the names names = [] for line in lines : - if re.match("\S", line) : + if re.match(r"\S", line) : names.append(line.rstrip()) # It's not fair to always put the same people at the head of the list random.shuffle(names) -- cgit v1.2.3 From ab9cb6fcd96c1c29650d844b5fd76e2ebbf5f2df Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Fri, 14 Jun 2024 20:43:09 -0400 Subject: Introduce LL.atexit(), internal lua_emplace(), lua_toclass(). Publish new LL.atexit() function that accepts a Lua function (or C++ closure) and saves it (in Registry["atexit"] table) to call later. Make ~LuaState() walk the Registry["atexit"] table, if it exists, calling each function appended to that table. (Consider using that mechanism to clean up a LuaListener, if one was instantiated. Possibly also use for p.s. leap.run()? But that's run after every expr() call, instead of only at ~LuaState() time. Pragmatically, though, the distinction only matters for a LUA Debug Console LUA string with "clean lua_State" unchecked.) For use by future lua_function() entry points, lua_emplace(ctor args...) pushes a Lua userdata object containing a newly-constructed T instance -- actually a std::optional to avoid double destruction. lua_emplace() is specifically intended to be usable even for T with a nontrivial destructor: it gives the userdata a metatable with a __gc function that destroys the contained T instance when the userdata is garbage collected. But since garbage collection doesn't guarantee to clean up global variables with __gc methods, lua_emplace() also uses LL.atexit() to ensure that ~T() will run when the LuaState is destroyed. The companion to lua_emplace() is lua_toclass(), which returns a non-nullptr T* if the referenced index is in fact a userdata created by lua_emplace() for the same T, that has not yet been destroyed. This lets C++ code access a T previously embedded in Lua userdata. --- indra/llcommon/lua_function.cpp | 76 +++++++++++++++- indra/llcommon/lua_function.h | 186 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 261 insertions(+), 1 deletion(-) diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 7c80f65ff9..edd49feed9 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -489,6 +489,37 @@ void LuaState::initLuaState() LuaState::~LuaState() { + // We're just about to destroy this lua_State mState. lua_close() doesn't + // implicitly garbage-collect everything, so (for instance) any lingering + // objects with __gc metadata methods aren't cleaned up. This is why we + // provide atexit(). + luaL_checkstack(mState, 3, nullptr); + // look up Registry["atexit"] + lua_getfield(mState, LUA_REGISTRYINDEX, "atexit"); + // stack contains Registry["atexit"] + if (lua_istable(mState, -1)) + { + lua_pushnil(mState); // first key + while (lua_next(mState, -2)) + { + // stack contains Registry["atexit"], key, value + // Call value(), no args, no return values. + // Use lua_pcall() because errors in any one atexit() function + // shouldn't cancel the rest of them. + if (lua_pcall(mState, 0, 0, 0) != LUA_OK) + { + auto error{ lua_tostdstring(mState, -1) }; + LL_WARNS("Lua") << "atexit() function error: " << error << LL_ENDL; + // pop error message + lua_pop(mState, 1); + } + // Normally we would pop value, keeping the key for the next + // iteration. But lua_pcall() has already popped the value. + } + } + // pop Registry["atexit"] (either table or nil) + lua_pop(mState, 1); + // Did somebody call obtainListener() on this LuaState? // That is, is there a LuaListener key in its registry? LuaListener::destruct(getListener()); @@ -509,7 +540,7 @@ bool LuaState::checkLua(const std::string& desc, int r) mError = lua_tostring(mState, -1); lua_pop(mState, 1); - LL_WARNS() << desc << ": " << mError << LL_ENDL; + LL_WARNS("Lua") << desc << ": " << mError << LL_ENDL; return false; } return true; @@ -685,6 +716,49 @@ LuaListener::ptr_t LuaState::obtainListener(lua_State* L) return listener; } +/***************************************************************************** +* atexit() +*****************************************************************************/ +lua_function(atexit, "register a Lua function to be called at script termination") +{ + luaL_checkstack(L, 4, nullptr); + // 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", "insert" + // look up the "atexit" table in the Registry + lua_getfield(L, LUA_REGISTRYINDEX, "atexit"); + // stack contains function, "table", "insert", Registry["atexit"] + if (! lua_istable(L, -1)) + { + llassert(lua_isnil(L, -1)); + // stack contains function, "table", "insert", nil + lua_pop(L, 1); + // make a new, empty table + lua_newtable(L); + // stack contains function, "table", "insert", {} + // duplicate the table reference on the stack + lua_pushvalue(L, -1); + // stack contains function, "table", "insert", {}, {} + // store the new empty "atexit" table to the Registry, leaving a + // reference on the stack + lua_setfield(L, LUA_REGISTRYINDEX, "atexit"); + } + // stack contains function, "table", "insert", Registry["atexit"] + // we were called with a Lua function to append to that Registry["atexit"] + // table -- push that function + lua_pushvalue(L, 1); // or -4 + // stack contains function, "table", "insert", Registry["atexit"], function + // call table.insert(atexit, function) + // don't use pcall(): if there's an error, let it propagate + lua_call(L, 2, 0); + // stack contains function, "table" -- pop everything + lua_settop(L, 0); + return 0; +} + /***************************************************************************** * LuaPopper class *****************************************************************************/ diff --git a/indra/llcommon/lua_function.h b/indra/llcommon/lua_function.h index e7013f92c6..5bbcbc441f 100644 --- a/indra/llcommon/lua_function.h +++ b/indra/llcommon/lua_function.h @@ -17,13 +17,19 @@ #include "luau/luaconf.h" #include "luau/lualib.h" #include "fsyspath.h" +#include "llerror.h" #include "stringize.h" #include // std::uncaught_exceptions() #include // std::shared_ptr +#include +#include #include // std::pair class LuaListener; +/***************************************************************************** +* lluau namespace utility functions +*****************************************************************************/ namespace lluau { // luau defines luaL_error() as void, but we want to use the Lua idiom of @@ -62,6 +68,9 @@ 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 */ @@ -110,6 +119,9 @@ private: std::string mError; }; +/***************************************************************************** +* LuaPopper +*****************************************************************************/ /** * LuaPopper is an RAII struct whose role is to pop some number of entries * from the Lua stack if the calling function exits early. @@ -133,6 +145,9 @@ struct LuaPopper int mCount; }; +/***************************************************************************** +* 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 @@ -182,6 +197,171 @@ int name##_luasub::call(lua_State* L) // ... supply method body here, referencing 'L' ... // } +/***************************************************************************** +* lua_emplace(), lua_toclass() +*****************************************************************************/ +namespace { + +// this closure function retrieves its bound argument to pass to +// lua_emplace_gc() +template +int lua_emplace_call_gc(lua_State* L); +// this will be the function called by the new userdata's metatable's __gc() +template +int lua_emplace_gc(lua_State* L); +// name by which we'll store the new userdata's metatable in the Registry +template +std::string lua_emplace_metaname(const std::string& Tname = LLError::Log::classname()); + +} // anonymous namespace + +/** + * On the stack belonging to the passed lua_State, push a Lua userdata object + * with a newly-constructed C++ object std::optional(args...). The new + * userdata has a metadata table with a __gc() function to ensure that when + * the userdata instance is garbage-collected, ~T() is called. + * + * We wrap the userdata object as std::optional so we can explicitly + * destroy the contained T, and detect that we've done so. + * + * Usage: + * lua_emplace(L, T constructor args...); + */ +template +void lua_emplace(lua_State* L, ARGS&&... args) +{ + using optT = std::optional; + luaL_checkstack(L, 3, nullptr); + auto ptr = lua_newuserdata(L, sizeof(optT)); + // 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(optT)) == 0); + // Construct our T there using placement new + new (ptr) optT(std::in_place, std::forward(args)...); + // stack is now initialized userdata containing our T instance + + // Find or create the metatable shared by all userdata instances holding + // C++ type T. We want it to be shared across instances, but it must be + // type-specific because its __gc field is lua_emplace_gc. + auto Tname{ LLError::Log::classname() }; + auto metaname{ lua_emplace_metaname(Tname) }; + if (luaL_newmetatable(L, metaname.c_str())) + { + // just created it: populate it + auto gcname{ stringize("lua_emplace_gc<", Tname, ">") }; + lua_pushcfunction(L, lua_emplace_gc, gcname.c_str()); + // stack is userdata, metatable, lua_emplace_gc + lua_setfield(L, -2, "__gc"); + } + // stack is userdata, metatable + lua_setmetatable(L, -2); + // Stack is now userdata, initialized with T(args), + // with metatable.__gc pointing to lua_emplace_gc. + // But wait, there's more! Use our atexit() function to ensure that this + // C++ object is eventually cleaned up even if the garbage collector never + // gets around to it. + lua_getglobal(L, "LL"); + // stack contains userdata, LL + lua_getfield(L, -1, "atexit"); + // stack contains userdata, LL, LL.atexit + // duplicate userdata + lua_pushvalue(L, -3); + // stack contains userdata, LL, LL.atexit, userdata + // push a closure binding (lua_emplace_call_gc, userdata) + auto callgcname{ stringize("lua_emplace_call_gc<", Tname, ">") }; + lua_pushcclosure(L, lua_emplace_call_gc, callgcname.c_str(), 1); + // stack contains userdata, LL, LL.atexit, closure + // Call LL.atexit(closure) + lua_call(L, 1, 0); + // stack contains userdata, LL + lua_pop(L, 1); + // stack contains userdata -- return that +} + +namespace { + +// passed to LL.atexit(closure(lua_emplace_call_gc, userdata)); +// retrieves bound userdata to pass to lua_emplace_gc() +template +int lua_emplace_call_gc(lua_State* L) +{ + luaL_checkstack(L, 1, nullptr); + // retrieve the first (only) bound upvalue and push to stack top as the + // argument for lua_emplace_gc() + lua_pushvalue(L, lua_upvalueindex(1)); + return lua_emplace_gc(L); +} + +// set as metatable(userdata).__gc to be called by the garbage collector +template +int lua_emplace_gc(lua_State* L) +{ + using optT = std::optional; + // We're called with userdata on the stack holding an instance of type T. + auto ptr = lua_touserdata(L, -1); + llassert(ptr); + // Destroy the T object contained in optT at the void* address ptr. If + // in future lua_emplace() must manually align our optT* within the + // Lua-provided void*, derive optT* from ptr. + static_cast(ptr)->reset(); + // pop the userdata + lua_pop(L, 1); + return 0; +} + +template +std::string lua_emplace_metaname(const std::string& Tname) +{ + return stringize("lua_emplace_", Tname, "_meta"); +} + +} // anonymous namespace + +/** + * If the value at the passed acceptable index is a full userdata created by + * lua_emplace() -- that is, the userdata contains a non-empty + * std::optional -- return a pointer to the contained T instance. Otherwise + * (index is not a full userdata; userdata is not of type std::optional; + * std::optional is empty) return nullptr. + */ +template +T* lua_toclass(lua_State* L, int index) +{ + using optT = std::optional; + luaL_checkstack(L, 2, nullptr); + // get void* pointer to userdata (if that's what it is) + auto ptr{ lua_touserdata(L, index) }; + if (! ptr) + return nullptr; + // push the metatable for this userdata, if any + if (! lua_getmetatable(L, index)) + return nullptr; + // now push the metatable created by lua_emplace() + auto metaname{ lua_emplace_metaname() }; + luaL_getmetatable(L, metaname.c_str()); + auto equal{ lua_equal(L, -1, -2) }; + // Having compared the userdata's metatable with the one set by + // lua_emplace(), we no longer need either metatable on the stack. + lua_pop(L, 2); + if (! equal) + return nullptr; + // Derive the optT* from ptr. If in future lua_emplace() must manually + // align our optT* within the Lua-provided void*, adjust accordingly. + optT* tptr(ptr); + // make sure our optT isn't empty + if (! *tptr) + return nullptr; + // looks like we still have a non-empty optT: return the *address* of the + // value() reference + return &tptr->value(); +} + +/***************************************************************************** +* 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. @@ -202,6 +382,9 @@ private: 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. @@ -220,6 +403,9 @@ 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 -- cgit v1.2.3 From 5b6a5c757deaba3c2b361eb49f2e61630fe3eb47 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Mon, 17 Jun 2024 11:18:09 -0400 Subject: Store script's LuaListener in userdata in lua_State's Registry. Instead of deriving LuaListener from LLInstanceTracker with an int key, generating a unique int key and storing that key in the Registry, use new lua_emplace() to store the LuaListener directly in a Lua userdata object in the Lua Registry. Because lua_emplace() uses LL.atexit() to guarantee that ~LuaState will destroy the T object, we no longer need ~LuaState() to make a special call specifically to destroy the LuaListener, if any. So we no longer need LuaState::getListener() separate from obtainListener(). Since LuaListener is no longer an LLInstanceTracker subclass, make LuaState::obtainListener() return LuaListener& rather than LuaListener::ptr_t. --- indra/llcommon/lua_function.cpp | 61 +++++++++++-------------------- indra/llcommon/lua_function.h | 16 +++----- indra/llcommon/lualistener.cpp | 30 +++++---------- indra/llcommon/lualistener.h | 20 +--------- indra/newview/llluamanager.cpp | 10 ++--- indra/newview/tests/llluamanager_test.cpp | 2 +- 6 files changed, 44 insertions(+), 95 deletions(-) diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index edd49feed9..cd1a0cd562 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -520,10 +520,6 @@ LuaState::~LuaState() // pop Registry["atexit"] (either table or nil) lua_pop(mState, 1); - // Did somebody call obtainListener() on this LuaState? - // That is, is there a LuaListener key in its registry? - LuaListener::destruct(getListener()); - lua_close(mState); if (mCallback) @@ -677,43 +673,30 @@ std::pair LuaState::expr(const std::string& desc, const std::string& return result; } -LuaListener::ptr_t LuaState::getListener(lua_State* L) +LuaListener& LuaState::obtainListener(lua_State* L) { - // have to use one more stack slot - luaL_checkstack(L, 1, nullptr); - LuaListener::ptr_t listener; - // Does this lua_State already have a LuaListener stored in the registry? - auto keytype{ lua_getfield(L, LUA_REGISTRYINDEX, "event.listener") }; - llassert(keytype == LUA_TNIL || keytype == LUA_TNUMBER); - if (keytype == LUA_TNUMBER) + luaL_checkstack(L, 2, nullptr); + lua_getfield(L, LUA_REGISTRYINDEX, "LuaListener"); + // compare lua_type() because lua_isuserdata() also accepts light userdata + if (lua_type(L, -1) != LUA_TUSERDATA) { - // We do already have a LuaListener. Retrieve it. - int isint; - listener = LuaListener::getInstance(lua_tointegerx(L, -1, &isint)); - // Nobody should have destroyed this LuaListener instance! - llassert(isint && listener); + llassert(lua_type(L, -1) == LUA_TNIL); + lua_pop(L, 1); + // push a userdata containing new LuaListener, binding L + lua_emplace(L, L); + // duplicate the top stack entry so we can store one copy + lua_pushvalue(L, -1); + lua_setfield(L, LUA_REGISTRYINDEX, "LuaListener"); } - // pop the int "event.listener" key + // At this point, one way or the other, the stack top should be (a Lua + // userdata containing) our LuaListener. + LuaListener* listener{ lua_toclass(L, -1) }; + // userdata objects created by lua_emplace() are bound on the atexit() + // queue, and are thus never garbage collected: they're destroyed only + // when ~LuaState() walks that queue. 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; -} - -LuaListener::ptr_t LuaState::obtainListener(lua_State* L) -{ - auto listener{ getListener(L) }; - if (! listener) - { - // have to use one more stack slot - luaL_checkstack(L, 1, nullptr); - // instantiate a new LuaListener, binding the L state -- but use a - // no-op deleter: we do NOT want this ptr_t to manage the lifespan of - // this new LuaListener! - listener.reset(new LuaListener(L), [](LuaListener*){}); - // set its key in the field where we'll look for it later - lua_pushinteger(L, listener->getKey()); - lua_setfield(L, LUA_REGISTRYINDEX, "event.listener"); - } - return listener; + return *listener; } /***************************************************************************** @@ -938,10 +921,10 @@ lua_function( lua_settop(L, 0); auto& outpump{ LLEventPumps::instance().obtain("lua output") }; - auto listener{ LuaState::obtainListener(L) }; + 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") }; + auto reply{ llcoro::postAndSuspend(request, listener.getCommandName(), replyPump, "reply") }; reply.erase("reqid"); if (auto error = reply["error"]; error.isString()) diff --git a/indra/llcommon/lua_function.h b/indra/llcommon/lua_function.h index 5bbcbc441f..8b93053a46 100644 --- a/indra/llcommon/lua_function.h +++ b/indra/llcommon/lua_function.h @@ -102,16 +102,10 @@ public: operator lua_State*() const { return mState; } - // Return LuaListener for this LuaState if we already have one, else empty - // shared_ptr. - std::shared_ptr getListener() { return getListener(mState); } - // Find or create LuaListener for this LuaState, returning its ptr_t. - std::shared_ptr obtainListener() { return obtainListener(mState); } - // Return LuaListener for passed lua_State if we already have one, else - // empty shared_ptr. - static std::shared_ptr getListener(lua_State* L); - // Find or create LuaListener for passed lua_State, returning its ptr_t. - static std::shared_ptr obtainListener(lua_State* L); + // 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); private: script_finished_fn mCallback; @@ -350,7 +344,7 @@ T* lua_toclass(lua_State* L, int index) return nullptr; // Derive the optT* from ptr. If in future lua_emplace() must manually // align our optT* within the Lua-provided void*, adjust accordingly. - optT* tptr(ptr); + optT* tptr(static_cast(ptr)); // make sure our optT isn't empty if (! *tptr) return nullptr; diff --git a/indra/llcommon/lualistener.cpp b/indra/llcommon/lualistener.cpp index 5c4989e891..6cb87e8af2 100644 --- a/indra/llcommon/lualistener.cpp +++ b/indra/llcommon/lualistener.cpp @@ -15,8 +15,7 @@ #include "lualistener.h" // STL headers // std headers -#include // std::rand() -#include // std::memcpy() +#include // std::quoted() // external library headers #include "luau/lua.h" // other Linden headers @@ -28,11 +27,11 @@ const int MAX_QSIZE = 1000; std::ostream& operator<<(std::ostream& out, const LuaListener& self) { - return out << "LuaListener(" << self.getReplyName() << ", " << self.getCommandName() << ")"; + return out << "LuaListener(" << std::quoted(self.mCoroName) << ", " + << self.getReplyName() << ", " << self.getCommandName() << ")"; } LuaListener::LuaListener(lua_State* L): - super(getUniqueKey()), mCoroName(LLCoros::getName()), mListener(new LLLeapListener( "LuaListener", @@ -49,24 +48,13 @@ LuaListener::LuaListener(lua_State* L): // viewer shutdown, close the queue to wake up getNext(). mQueue.close(); })) -{} +{ + LL_DEBUGS("Lua") << "LuaListener(" << std::quoted(mCoroName) << ")" << LL_ENDL; +} LuaListener::~LuaListener() -{} - -int LuaListener::getUniqueKey() { - // Find a random key that does NOT already correspond to a LuaListener - // instance. Passing a duplicate key to LLInstanceTracker would do Bad - // Things. - int key; - do - { - key = std::rand(); - } while (LuaListener::getInstance(key)); - // This is theoretically racy, if we were instantiating new - // LuaListeners on multiple threads. Don't. - return key; + LL_DEBUGS("Lua") << "~LuaListener(" << std::quoted(mCoroName) << ")" << LL_ENDL; } std::string LuaListener::getReplyName() const @@ -86,7 +74,7 @@ bool LuaListener::queueEvent(const std::string& pump, const LLSD& data) // 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 " << getReplyName() + LL_WARNS("Lua") << "LuaListener queue for " << mCoroName << " exceeds " << MAX_QSIZE << ": " << size << " -- discarding event" << LL_ENDL; } @@ -107,7 +95,7 @@ LuaListener::PumpData LuaListener::getNext() catch (const LLThreadSafeQueueInterrupt&) { // mQueue has been closed. The only way that happens is when we detect - // viewer shutdown. Terminate the calling coroutine. + // viewer shutdown. Terminate the calling Lua coroutine. LLCoros::checkStop(); return {}; } diff --git a/indra/llcommon/lualistener.h b/indra/llcommon/lualistener.h index 85fb093cd6..68131dfa27 100644 --- a/indra/llcommon/lualistener.h +++ b/indra/llcommon/lualistener.h @@ -12,8 +12,7 @@ #if ! defined(LL_LUALISTENER_H) #define LL_LUALISTENER_H -#include "llevents.h" -#include "llinstancetracker.h" +#include "llevents.h" // LLTempBoundListener #include "llsd.h" #include "llthreadsafequeue.h" #include // std::ostream @@ -27,25 +26,11 @@ class LLLeapListener; /** * LuaListener is based on LLLeap. It serves an analogous function. * - * Each LuaListener instance has an int key, generated randomly to - * inconvenience malicious Lua scripts wanting to mess with others. The idea - * is that a given lua_State stores in its Registry: - * - "event.listener": the int key of the corresponding LuaListener, if any - * The original thought was that LuaListener would itself store the Lua - * function -- but surprisingly, there is no C/C++ type in the API that stores - * a Lua function. - * - * (We considered storing in "event.listener" the LuaListener pointer itself - * as a light userdata, but the problem would be if Lua code overwrote that. - * We want to prevent any Lua script from crashing the viewer, intentionally - * or otherwise. Safer to use a key lookup.) - * * Like LLLeap, each LuaListener instance also has an associated * LLLeapListener to respond to LLEventPump management commands. */ -class LuaListener: public LLInstanceTracker +class LuaListener { - using super = LLInstanceTracker; public: LuaListener(lua_State* L); @@ -68,7 +53,6 @@ public: friend std::ostream& operator<<(std::ostream& out, const LuaListener& self); private: - static int getUniqueKey(); bool queueEvent(const std::string& pump, const LLSD& data); LLThreadSafeQueue mQueue; diff --git a/indra/newview/llluamanager.cpp b/indra/newview/llluamanager.cpp index 5aa47d7566..8b6f7a4698 100644 --- a/indra/newview/llluamanager.cpp +++ b/indra/newview/llluamanager.cpp @@ -139,10 +139,10 @@ lua_function(get_event_pumps, "post_on(commandpump, ...) to engage LLEventAPI operations (see helpleap()).") { luaL_checkstack(L, 2, nullptr); - auto listener{ LuaState::obtainListener(L) }; + auto& listener{ LuaState::obtainListener(L) }; // return the reply pump name and the command pump name on caller's lua_State - lua_pushstdstring(L, listener->getReplyName()); - lua_pushstdstring(L, listener->getCommandName()); + lua_pushstdstring(L, listener.getReplyName()); + lua_pushstdstring(L, listener.getCommandName()); return 2; } @@ -153,8 +153,8 @@ lua_function(get_event_next, "event becomes available.") { luaL_checkstack(L, 2, nullptr); - auto listener{ LuaState::obtainListener(L) }; - const auto& [pump, data]{ listener->getNext() }; + auto& listener{ LuaState::obtainListener(L) }; + const auto& [pump, data]{ listener.getNext() }; lua_pushstdstring(L, pump); lua_pushllsd(L, data); lluau::set_interrupts_counter(L, 0); diff --git a/indra/newview/tests/llluamanager_test.cpp b/indra/newview/tests/llluamanager_test.cpp index cf1bf25b5c..2d525f7913 100644 --- a/indra/newview/tests/llluamanager_test.cpp +++ b/indra/newview/tests/llluamanager_test.cpp @@ -398,7 +398,7 @@ namespace tut LuaState L; auto future = LLLUAmanager::startScriptLine(L, lua); - auto replyname{ L.obtainListener()->getReplyName() }; + auto replyname{ L.obtainListener().getReplyName() }; auto& replypump{ LLEventPumps::instance().obtain(replyname) }; // LuaState::expr() periodically interrupts a running chunk to ensure // the rest of our coroutines get cycles. Nonetheless, for this test -- cgit v1.2.3 From aff78224a026bbf17e6ac4818228c0e1814c4226 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 18 Jun 2024 09:11:29 -0400 Subject: Make lluau::source_path() report top-level script path. source_path() previously reported the path of the module containing the current (lowest-level) Lua function. The effect was that the Floater.lua module would always try to look up the XUI file relative to scripts/lua/require. It makes more intuitive sense to make source_path() return the path containing the top-level script, so that a script engaging the Floater.lua module looks for the XUI file relative to the script. --- indra/llcommon/lua_function.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index cd1a0cd562..3e3934c9c1 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -75,8 +75,11 @@ fsyspath lluau::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 lua_Debug ar; - lua_getinfo(L, 1, "s", &ar); + lua_getinfo(L, lua_stackdepth(L), "s", &ar); return ar.source; } -- cgit v1.2.3 From 6bbd39f54a71a1d223c6e74b47c6b0cf9f72eb7e Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 18 Jun 2024 09:13:44 -0400 Subject: lua_emplace() should permit GC despite LL.atexit() safety net. lua_emplace() was passing LL.atexit() a closure binding the new userdata with a cleanup function. The trouble with that was that a strong reference to the new userdata would prevent it ever being garbage collected, even if that was the only remaining reference. Instead, create a new weak table referencing the userdata, and bind that into the cleanup function's closure. Then if the only remaining reference to the userdata is from the weak table, the userdata can be collected. Make lua_emplace_call_gc() check the bound weak table in case the userdata has in fact been collected. Also, in lua_toclass(), use luaL_checkudata() to synopsize comparing the putative userdata's metatable against the one synthesized by lua_emplace(). This saves several explicit steps. --- indra/llcommon/lua_function.h | 101 +++++++++++++++++++++++++++++++----------- 1 file changed, 74 insertions(+), 27 deletions(-) diff --git a/indra/llcommon/lua_function.h b/indra/llcommon/lua_function.h index 8b93053a46..7a3d9e7dd7 100644 --- a/indra/llcommon/lua_function.h +++ b/indra/llcommon/lua_function.h @@ -213,19 +213,21 @@ std::string lua_emplace_metaname(const std::string& Tname = LLError::Log::classn * On the stack belonging to the passed lua_State, push a Lua userdata object * with a newly-constructed C++ object std::optional(args...). The new * userdata has a metadata table with a __gc() function to ensure that when - * the userdata instance is garbage-collected, ~T() is called. + * the userdata instance is garbage-collected, ~T() is called. Also call + * LL.atexit(lua_emplace_call_gc(object)) to make ~LuaState() call ~T(). * * We wrap the userdata object as std::optional so we can explicitly * destroy the contained T, and detect that we've done so. * * Usage: * lua_emplace(L, T constructor args...); + * // L's Lua stack top is now a userdata containing T */ template void lua_emplace(lua_State* L, ARGS&&... args) { using optT = std::optional; - luaL_checkstack(L, 3, nullptr); + luaL_checkstack(L, 5, nullptr); auto ptr = lua_newuserdata(L, sizeof(optT)); // stack is uninitialized userdata // For now, assume (but verify) that lua_newuserdata() returns a @@ -254,38 +256,94 @@ void lua_emplace(lua_State* L, ARGS&&... args) lua_setmetatable(L, -2); // Stack is now userdata, initialized with T(args), // with metatable.__gc pointing to lua_emplace_gc. + // But wait, there's more! Use our atexit() function to ensure that this - // C++ object is eventually cleaned up even if the garbage collector never + // C++ object is eventually destroyed even if the garbage collector never // gets around to it. lua_getglobal(L, "LL"); // stack contains userdata, LL lua_getfield(L, -1, "atexit"); // stack contains userdata, LL, LL.atexit + // ditch LL + lua_replace(L, -2); + // stack contains userdata, LL.atexit + + // We have a bit of a problem here. We want to allow the garbage collector + // to collect the userdata if it must; but we also want to register a + // cleanup function to destroy the value if (usual case) it has NOT been + // garbage-collected. The problem is that if we bind into atexit()'s queue + // a strong reference to the userdata, we ensure that the garbage + // collector cannot collect it, making our metatable with __gc function + // completely moot. And we must assume that lua_pushcclosure() binds a + // strong reference to each value passed as a closure. + + // The solution is to use one more indirection: create a weak table whose + // sole entry is the userdata. If all other references to the new userdata + // are forgotten, so the only remaining reference is the weak table, the + // userdata can be collected. Then we can bind that weak table as the + // closure value for our cleanup function. + // The new weak table will have at most 1 array value, 0 other keys. + lua_createtable(L, 1, 0); + // stack contains userdata, LL.atexit, weak_table + if (luaL_newmetatable(L, "weak_values")) + { + // stack contains userdata, LL.atexit, weak_table, weak_values + // just created "weak_values" metatable: populate it + // Registry.weak_values = {__mode="v"} + lua_pushliteral(L, "v"); + // stack contains userdata, LL.atexit, weak_table, weak_values, "v" + lua_setfield(L, -2, "__mode"); + } + // stack contains userdata, LL.atexit, weak_table, weak_values + // setmetatable(weak_table, weak_values) + lua_setmetatable(L, -2); + // stack contains userdata, LL.atexit, weak_table + lua_pushinteger(L, 1); + // stack contains userdata, LL.atexit, weak_table, 1 // duplicate userdata - lua_pushvalue(L, -3); - // stack contains userdata, LL, LL.atexit, userdata - // push a closure binding (lua_emplace_call_gc, userdata) + lua_pushvalue(L, -4); + // stack contains userdata, LL.atexit, weak_table, 1, userdata + // weak_table[1] = userdata + lua_settable(L, -3); + // stack contains userdata, LL.atexit, weak_table + + // push a closure binding (lua_emplace_call_gc, weak_table) auto callgcname{ stringize("lua_emplace_call_gc<", Tname, ">") }; lua_pushcclosure(L, lua_emplace_call_gc, callgcname.c_str(), 1); - // stack contains userdata, LL, LL.atexit, closure + // stack contains userdata, LL.atexit, closure // Call LL.atexit(closure) lua_call(L, 1, 0); - // stack contains userdata, LL - lua_pop(L, 1); // stack contains userdata -- return that } namespace { -// passed to LL.atexit(closure(lua_emplace_call_gc, userdata)); +// passed to LL.atexit(closure(lua_emplace_call_gc, weak_table{userdata})); // retrieves bound userdata to pass to lua_emplace_gc() template int lua_emplace_call_gc(lua_State* L) { - luaL_checkstack(L, 1, nullptr); - // retrieve the first (only) bound upvalue and push to stack top as the - // argument for lua_emplace_gc() + luaL_checkstack(L, 2, nullptr); + // retrieve the first (only) bound upvalue and push to stack top lua_pushvalue(L, lua_upvalueindex(1)); + // This is the weak_table bound by lua_emplace(). Its one and only + // entry should be the lua_emplace() userdata -- unless userdata has + // been garbage collected. Retrieve weak_table[1]. + lua_pushinteger(L, 1); + // stack contains weak_table, 1 + lua_gettable(L, -2); + // stack contains weak_table, weak_table[1] + // If our userdata was garbage-collected, there is no weak_table[1], + // and we just retrieved nil. + if (lua_isnil(L, -1)) + { + lua_pop(L, 2); + return 0; + } + // stack contains weak_table, userdata + // ditch weak_table + lua_replace(L, -2); + // pass userdata to lua_emplace_gc() return lua_emplace_gc(L); } @@ -325,23 +383,12 @@ template T* lua_toclass(lua_State* L, int index) { using optT = std::optional; - luaL_checkstack(L, 2, nullptr); + // recreate the name lua_emplace() uses for its metatable + auto metaname{ lua_emplace_metaname() }; // get void* pointer to userdata (if that's what it is) - auto ptr{ lua_touserdata(L, index) }; + void* ptr{ luaL_checkudata(L, index, metaname.c_str()) }; if (! ptr) return nullptr; - // push the metatable for this userdata, if any - if (! lua_getmetatable(L, index)) - return nullptr; - // now push the metatable created by lua_emplace() - auto metaname{ lua_emplace_metaname() }; - luaL_getmetatable(L, metaname.c_str()); - auto equal{ lua_equal(L, -1, -2) }; - // Having compared the userdata's metatable with the one set by - // lua_emplace(), we no longer need either metatable on the stack. - lua_pop(L, 2); - if (! equal) - return nullptr; // Derive the optT* from ptr. If in future lua_emplace() must manually // align our optT* within the Lua-provided void*, adjust accordingly. optT* tptr(static_cast(ptr)); -- cgit v1.2.3 From 5cc4b42a3001be120e22f745460dbb76d8d8d018 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 18 Jun 2024 13:11:07 -0400 Subject: Make ~LuaState() walk Registry.atexit table backwards so cleanup happens in reverse order, as is conventional. Streamline LL.atexit() function: luaL_newmetatable() performs all the find-or-create named Registry table logic. --- indra/llcommon/lua_function.cpp | 67 ++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 37 deletions(-) diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 3e3934c9c1..7894c7b96a 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -496,17 +496,24 @@ LuaState::~LuaState() // implicitly garbage-collect everything, so (for instance) any lingering // objects with __gc metadata methods aren't cleaned up. This is why we // provide atexit(). - luaL_checkstack(mState, 3, nullptr); - // look up Registry["atexit"] + luaL_checkstack(mState, 2, nullptr); + // look up Registry.atexit lua_getfield(mState, LUA_REGISTRYINDEX, "atexit"); - // stack contains Registry["atexit"] + // stack contains Registry.atexit if (lua_istable(mState, -1)) { - lua_pushnil(mState); // first key - while (lua_next(mState, -2)) + // 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. + for (int i(lua_objlen(mState, -1)); i >= 1; --i) { - // stack contains Registry["atexit"], key, value - // Call value(), no args, no return values. + lua_pushinteger(mState, i); + // stack contains Registry.atexit, i + lua_gettable(mState, -2); + // stack contains Registry.atexit, 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. if (lua_pcall(mState, 0, 0, 0) != LUA_OK) @@ -516,11 +523,10 @@ LuaState::~LuaState() // pop error message lua_pop(mState, 1); } - // Normally we would pop value, keeping the key for the next - // iteration. But lua_pcall() has already popped the value. + // lua_pcall() has already popped atexit[i]: stack contains atexit } } - // pop Registry["atexit"] (either table or nil) + // pop Registry.atexit (either table or nil) lua_pop(mState, 1); lua_close(mState); @@ -710,37 +716,24 @@ lua_function(atexit, "register a Lua function to be called at script termination luaL_checkstack(L, 4, nullptr); // look up the global name "table" lua_getglobal(L, "table"); - // stack contains function, "table" + // stack contains function, table // look up table.insert lua_getfield(L, -1, "insert"); - // stack contains function, "table", "insert" - // look up the "atexit" table in the Registry - lua_getfield(L, LUA_REGISTRYINDEX, "atexit"); - // stack contains function, "table", "insert", Registry["atexit"] - if (! lua_istable(L, -1)) - { - llassert(lua_isnil(L, -1)); - // stack contains function, "table", "insert", nil - lua_pop(L, 1); - // make a new, empty table - lua_newtable(L); - // stack contains function, "table", "insert", {} - // duplicate the table reference on the stack - lua_pushvalue(L, -1); - // stack contains function, "table", "insert", {}, {} - // store the new empty "atexit" table to the Registry, leaving a - // reference on the stack - lua_setfield(L, LUA_REGISTRYINDEX, "atexit"); - } - // stack contains function, "table", "insert", Registry["atexit"] - // we were called with a Lua function to append to that Registry["atexit"] - // table -- push that function - lua_pushvalue(L, 1); // or -4 - // stack contains function, "table", "insert", Registry["atexit"], function - // call table.insert(atexit, function) + // 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, "table" -- pop everything + // stack contains function -- pop everything lua_settop(L, 0); return 0; } -- cgit v1.2.3 From c6001e6a20389b4d32813c7f61b56ff79f575723 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 18 Jun 2024 18:01:39 -0400 Subject: Improve diagnostic output from running 'require' module. --- indra/newview/llluamanager.cpp | 75 +++++++++++++++++++++++++++--------------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/indra/newview/llluamanager.cpp b/indra/newview/llluamanager.cpp index 8b6f7a4698..3ed72c34f3 100644 --- a/indra/newview/llluamanager.cpp +++ b/indra/newview/llluamanager.cpp @@ -454,45 +454,70 @@ bool LLRequireResolver::findModuleImpl(const std::string& absolutePath) void LLRequireResolver::runModule(const std::string& desc, const std::string& code) { // Here we just loaded a new module 'code', need to run it and get its result. - // Module needs to run in a new thread, isolated from the rest. - // Note: we create ML on main thread so that it doesn't inherit environment of L. - lua_State *GL = lua_mainthread(L); -// lua_State *ML = lua_newthread(GL); - // Try loading modules on Lua's main thread instead. - lua_State *ML = GL; - // lua_newthread() pushed the new thread object on GL's stack. Move to L's. -// lua_xmove(GL, L, 1); - - // new thread needs to have the globals sandboxed -// luaL_sandboxthread(ML); + lua_State *ML = lua_mainthread(L); { // If loadstring() returns (! LUA_OK) then there's an error message on // the stack. If it returns LUA_OK then the newly-loaded module code // is on the stack. - if (lluau::loadstring(ML, desc, code) == LUA_OK) + LL_DEBUGS("Lua") << "Loading module " << desc << LL_ENDL; + if (lluau::loadstring(ML, desc, code) != LUA_OK) { - // luau uses Lua 5.3's version of lua_resume(): - // run the coroutine on ML, "from" L, passing no arguments. -// int status = lua_resume(ML, L, 0); - // we expect one return value - int status = lua_pcall(ML, 0, 1, 0); + // error message on stack top + LL_DEBUGS("Lua") << "Error loading module " << desc << ": " + << lua_tostring(ML, -1) << LL_ENDL; + lua_pushliteral(ML, "loadstring: "); + // stack contains error, "loadstring: " + // swap: insert stack top at position -2 + lua_insert(ML, -2); + // stack contains "loadstring: ", error + lua_concat(ML, 2); + // stack contains "loadstring: " + error + } + else // module code on stack top + { + // push debug module + lua_getglobal(ML, "debug"); + // push debug.traceback + lua_getfield(ML, -1, "traceback"); + // stack contains module code, debug, debug.traceback + // ditch debug + lua_replace(ML, -2); + // stack contains module code, debug.traceback + // swap: insert stack top at position -2 + lua_insert(ML, -2); + // stack contains debug.traceback, module code + LL_DEBUGS("Lua") << "Loaded module " << desc << ", running" << LL_ENDL; + // no arguments, one return value + // pass debug.traceback as the error function + int status = lua_pcall(ML, 0, 1, -2); + // lua_pcall() has popped the module code and replaced it with its + // return value. Regardless of status or the type of the stack + // top, get rid of debug.traceback on the stack. + lua_remove(ML, -2); if (status == LUA_OK) { - if (lua_gettop(ML) == 0) - lua_pushfstring(ML, "module %s must return a value", desc.data()); - else if (!lua_istable(ML, -1) && !lua_isfunction(ML, -1)) + auto top{ lua_gettop(ML) }; + std::string type{ (top == 0)? "nothing" + : lua_typename(ML, lua_type(ML, -1)) }; + LL_DEBUGS("Lua") << "Module " << desc << " returned " << type << LL_ENDL; + if ((top == 0) || ! (lua_istable(ML, -1) || lua_isfunction(ML, -1))) + { lua_pushfstring(ML, "module %s must return a table or function, not %s", - desc.data(), lua_typename(ML, lua_type(ML, -1))); + desc.data(), type.data()); + } } else if (status == LUA_YIELD) { + LL_DEBUGS("Lua") << "Module " << desc << " yielded" << LL_ENDL; lua_pushfstring(ML, "module %s can not yield", desc.data()); } - else if (!lua_isstring(ML, -1)) + else { - lua_pushfstring(ML, "unknown error while running module %s", desc.data()); + llassert(lua_isstring(ML, -1)); + LL_DEBUGS("Lua") << "Module " << desc << " error: " + << lua_tostring(ML, -1) << LL_ENDL; } } } @@ -502,8 +527,4 @@ void LLRequireResolver::runModule(const std::string& desc, const std::string& co { lua_xmove(ML, L, 1); } - // remove ML from L's stack -// lua_remove(L, -2); -// // DON'T call lua_close(ML)! Since ML is only a thread of L, corrupts L too! -// lua_close(ML); } -- cgit v1.2.3 From bac8ced279bb4b3ec49e497b75021b86a0b4d857 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 18 Jun 2024 18:02:47 -0400 Subject: Use LL_DEBUGS("Lua") for LuaLog. We might decide to leave some of them in place. --- indra/llcommon/lua_function.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indra/llcommon/lua_function.h b/indra/llcommon/lua_function.h index 7a3d9e7dd7..9cdd5665dc 100644 --- a/indra/llcommon/lua_function.h +++ b/indra/llcommon/lua_function.h @@ -473,7 +473,7 @@ public: template void operator()(ARGS&&... args) { - LL_INFOS("Lua") << mBlock << ' '; + LL_DEBUGS("Lua") << mBlock << ' '; stream_to(LL_CONT, std::forward(args)...); LL_ENDL; } -- cgit v1.2.3 From b7a70eb7ee3451dae596ddd691666eb83ac9350c Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 18 Jun 2024 18:09:16 -0400 Subject: Initialize lua_Debug lluau::source_path() passes to lua_getinfo(). On Mac it doesn't seem to matter, but on Windows, leaving it uninitialized can produce garbage results and even crash the coroutine. This seems strange, since we've been assuming lua_getinfo() treats its lua_Debug* as output-only. --- indra/llcommon/lua_function.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 3e3934c9c1..283dfa3c94 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -78,7 +78,7 @@ fsyspath lluau::source_path(lua_State* L) // In particular: // passing level=1 gets you info about the deepest function call // passing level=lua_stackdepth() gets you info about the topmost script - lua_Debug ar; + lua_Debug ar{}; lua_getinfo(L, lua_stackdepth(L), "s", &ar); return ar.source; } -- cgit v1.2.3 From 2739154eaa04877b0d19d1dfc56fc1679aa6bb98 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 19 Jun 2024 08:56:51 -0400 Subject: Try harder to keep Luau's lua_getinfo() from crashing. --- indra/llcommon/lua_function.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 7e99480201..e76bd55dbb 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -78,8 +78,13 @@ fsyspath lluau::source_path(lua_State* L) // 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{}; - lua_getinfo(L, lua_stackdepth(L), "s", &ar); + for (int i(0), depth(lua_stackdepth(L)); i <= depth; ++i) + { + lua_getinfo(L, i, "s", &ar); + } return ar.source; } -- cgit v1.2.3 From ef596c44fce4a1059b8d79aff1c73db5ce628169 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 19 Jun 2024 09:43:27 -0400 Subject: Improve LL.help() function. The help string for each lua_function() must restate the function name and its arguments. The help string is all that's shown; unless it restates the function name, LL.help() output lists terse explanations for functions whose names are not shown. Make help() prepend "LL." to help output, because these functions must be accessed via the "builtin" LL table instead of directly populating the global Lua namespace. Similarly, before string name lookup, remove "LL." prefix if specified. --- indra/llcommon/lua_function.cpp | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index e76bd55dbb..255385b8c4 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -28,6 +28,7 @@ #include "lleventcoro.h" #include "llsd.h" #include "llsdutil.h" +#include "llstring.h" #include "lualistener.h" #include "stringize.h" @@ -716,7 +717,8 @@ LuaListener& LuaState::obtainListener(lua_State* L) /***************************************************************************** * atexit() *****************************************************************************/ -lua_function(atexit, "register a Lua function to be called at script termination") +lua_function(atexit, "atexit(function): " + "register Lua function to be called at script termination") { luaL_checkstack(L, 4, nullptr); // look up the global name "table" @@ -804,7 +806,7 @@ std::pair LuaFunction::getState() /***************************************************************************** * source_path() *****************************************************************************/ -lua_function(source_path, "return the source path of the running Lua script") +lua_function(source_path, "source_path(): return the source path of the running Lua script") { luaL_checkstack(L, 1, nullptr); lua_pushstdstring(L, lluau::source_path(L).u8string()); @@ -814,7 +816,7 @@ lua_function(source_path, "return the source path of the running Lua script") /***************************************************************************** * source_dir() *****************************************************************************/ -lua_function(source_dir, "return the source directory of the running Lua script") +lua_function(source_dir, "source_dir(): return the source directory of the running Lua script") { luaL_checkstack(L, 1, nullptr); lua_pushstdstring(L, lluau::source_path(L).parent_path().u8string()); @@ -824,7 +826,7 @@ lua_function(source_dir, "return the source directory of the running Lua script" /***************************************************************************** * abspath() *****************************************************************************/ -lua_function(abspath, +lua_function(abspath, "abspath(path): " "for given filesystem path relative to running script, return absolute path") { auto path{ lua_tostdstring(L, 1) }; @@ -836,7 +838,7 @@ lua_function(abspath, /***************************************************************************** * check_stop() *****************************************************************************/ -lua_function(check_stop, "ensure that a Lua script responds to viewer shutdown") +lua_function(check_stop, "check_stop(): ensure that a Lua script responds to viewer shutdown") { LLCoros::checkStop(); return 0; @@ -857,7 +859,7 @@ lua_function(help, for (const auto& [name, pair] : registry) { const auto& [fptr, helptext] = pair; - luapump.post(helptext); + luapump.post("LL." + helptext); } } else @@ -869,6 +871,7 @@ lua_function(help, if (lua_type(L, idx) == LUA_TSTRING) { arg = lua_tostdstring(L, idx); + LLStringUtil::removePrefix(arg, "LL."); } else if (lua_type(L, idx) == LUA_TFUNCTION) { @@ -887,7 +890,7 @@ lua_function(help, if (auto found = registry.find(arg); found != registry.end()) { - luapump.post(found->second.second); + luapump.post("LL." + found->second.second); } else { -- cgit v1.2.3 From 165b3cb04266f96f7fc48f542f915b93a4795aaf Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 19 Jun 2024 10:10:53 -0400 Subject: Move popup.lua to require subdir with the rest of the modules. --- indra/newview/scripts/lua/popup.lua | 32 ----------------------------- indra/newview/scripts/lua/require/popup.lua | 32 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 32 deletions(-) delete mode 100644 indra/newview/scripts/lua/popup.lua create mode 100644 indra/newview/scripts/lua/require/popup.lua diff --git a/indra/newview/scripts/lua/popup.lua b/indra/newview/scripts/lua/popup.lua deleted file mode 100644 index 8a01ab7836..0000000000 --- a/indra/newview/scripts/lua/popup.lua +++ /dev/null @@ -1,32 +0,0 @@ -local leap = require 'leap' - --- notification is any name defined in notifications.xml as --- --- vars is a table providing values for [VAR] substitution keys in the --- notification body. -local popup_meta = { - -- setting this function as getmetatable(popup).__call() means this gets - -- called when a consumer calls popup(notification, vars, payload) - __call = function(self, notification, vars, payload) - return leap.request('LLNotifications', - {op='requestAdd', name=notification, - substitutions=vars, - payload=payload}) - end -} - -local popup = setmetatable({}, popup_meta) - -function popup:alert(message) - return self('GenericAlert', {MESSAGE=message}) -end - -function popup:alertOK(message) - return self('GenericAlertOK', {MESSAGE=message}) -end - -function popup:alertYesCancel(message) - return self('GenericAlertYesCancel', {MESSAGE=message}) -end - -return popup diff --git a/indra/newview/scripts/lua/require/popup.lua b/indra/newview/scripts/lua/require/popup.lua new file mode 100644 index 0000000000..8a01ab7836 --- /dev/null +++ b/indra/newview/scripts/lua/require/popup.lua @@ -0,0 +1,32 @@ +local leap = require 'leap' + +-- notification is any name defined in notifications.xml as +-- +-- vars is a table providing values for [VAR] substitution keys in the +-- notification body. +local popup_meta = { + -- setting this function as getmetatable(popup).__call() means this gets + -- called when a consumer calls popup(notification, vars, payload) + __call = function(self, notification, vars, payload) + return leap.request('LLNotifications', + {op='requestAdd', name=notification, + substitutions=vars, + payload=payload}) + end +} + +local popup = setmetatable({}, popup_meta) + +function popup:alert(message) + return self('GenericAlert', {MESSAGE=message}) +end + +function popup:alertOK(message) + return self('GenericAlertOK', {MESSAGE=message}) +end + +function popup:alertYesCancel(message) + return self('GenericAlertYesCancel', {MESSAGE=message}) +end + +return popup -- cgit v1.2.3 From 350225e54ba02a7523e17106936a4ce44af91e55 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 20 Jun 2024 10:55:19 -0400 Subject: Give popup() the ability to not wait; add popup:tip(message). popup:tip() engages 'SystemMessageTip'. --- indra/newview/scripts/lua/require/popup.lua | 33 +++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/indra/newview/scripts/lua/require/popup.lua b/indra/newview/scripts/lua/require/popup.lua index 8a01ab7836..3aaadf85ba 100644 --- a/indra/newview/scripts/lua/require/popup.lua +++ b/indra/newview/scripts/lua/require/popup.lua @@ -1,17 +1,34 @@ local leap = require 'leap' +local mapargs = require 'mapargs' -- notification is any name defined in notifications.xml as -- -- vars is a table providing values for [VAR] substitution keys in the --- notification body. +-- notification body +-- payload prepopulates the response table +-- wait=false means fire and forget, otherwise wait for user response local popup_meta = { -- setting this function as getmetatable(popup).__call() means this gets -- called when a consumer calls popup(notification, vars, payload) - __call = function(self, notification, vars, payload) - return leap.request('LLNotifications', - {op='requestAdd', name=notification, - substitutions=vars, - payload=payload}) + __call = function(self, ...) + local args = mapargs('notification,vars,payload,wait', ...) + -- we use convenience argument names different from 'LLNotifications' + -- listener + args.name = args.notification + args.notification = nil + args.substitutions = args.vars + args.vars = nil + local wait = args.wait + args.wait = nil + args.op = 'requestAdd' + -- Specifically test (wait == false), NOT (not wait), because we treat + -- nil (omitted, default true) differently than false (explicitly + -- DON'T wait). + if wait == false then + leap.send('LLNotifications', args) + else + return leap.request('LLNotifications', args).response + end end } @@ -29,4 +46,8 @@ function popup:alertYesCancel(message) return self('GenericAlertYesCancel', {MESSAGE=message}) end +function popup:tip(message) + self{'SystemMessageTip', {MESSAGE=message}, wait=false} +end + return popup -- cgit v1.2.3 From 6d29c1fcf80441cc4619e672ca07469cc3efe100 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 20 Jun 2024 16:21:17 -0400 Subject: Use new popup.lua, which supersedes LLNotification.lua. Use ClassName(ctor args) for classes using util.classctor(). --- indra/newview/scripts/lua/LLNotification.lua | 15 --------------- indra/newview/scripts/lua/test_luafloater_speedometer.lua | 11 +++++------ 2 files changed, 5 insertions(+), 21 deletions(-) delete mode 100644 indra/newview/scripts/lua/LLNotification.lua diff --git a/indra/newview/scripts/lua/LLNotification.lua b/indra/newview/scripts/lua/LLNotification.lua deleted file mode 100644 index f47730d1cc..0000000000 --- a/indra/newview/scripts/lua/LLNotification.lua +++ /dev/null @@ -1,15 +0,0 @@ --- Engage the LLNotificationsListener LLEventAPI - -leap = require 'leap' - -local LLNotification = {} - -function LLNotification.add(name, substitutions) - leap.send('LLNotifications', {op='requestAdd', name=name, substitutions=substitutions}) -end - -function LLNotification.requestAdd(name, substitutions) - return leap.request('LLNotifications', {op='requestAdd', name=name, substitutions=substitutions})['response'] -end - -return LLNotification diff --git a/indra/newview/scripts/lua/test_luafloater_speedometer.lua b/indra/newview/scripts/lua/test_luafloater_speedometer.lua index a9d3a70330..af7189a2cb 100644 --- a/indra/newview/scripts/lua/test_luafloater_speedometer.lua +++ b/indra/newview/scripts/lua/test_luafloater_speedometer.lua @@ -1,10 +1,10 @@ local Floater = require 'Floater' local leap = require 'leap' -local LLNotification = require 'LLNotification' +local popup = require 'popup' local startup = require 'startup' local Timer = (require 'timers').Timer local max_speed = 0 -local flt = Floater:new("luafloater_speedometer.xml") +local flt = Floater("luafloater_speedometer.xml") startup.wait('STATE_STARTED') local timer @@ -13,8 +13,7 @@ function flt:floater_close(event_data) if timer then timer:cancel() end - msg = "Registered max speed: " .. string.format("%.2f", max_speed) .. " m/s"; - LLNotification.add('SystemMessageTip', {MESSAGE = msg}) + popup:tip(string.format("Registered max speed: %.2f m/s", max_speed)) end local function idle(event_data) @@ -24,9 +23,9 @@ local function idle(event_data) end msg = 'Are you sure you want to run this "speedometer" script?' -response = LLNotification.requestAdd('GenericAlertYesCancel', {MESSAGE = msg}) +response = popup:alertYesCancel(msg) if response.OK_okcancelbuttons then flt:show() - timer = Timer:new(1, idle, true) -- iterate + timer = Timer(1, idle, true) -- iterate end -- cgit v1.2.3 From 5dbca6bb3ed33cb8f19ea3871ce7ef9f07957088 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Fri, 21 Jun 2024 10:06:55 -0400 Subject: Move newer Lua modules to scripts/lua/require subdirectory. --- indra/newview/scripts/lua/LLChatListener.lua | 45 ------------- indra/newview/scripts/lua/login.lua | 19 ------ indra/newview/scripts/lua/mapargs.lua | 73 ---------------------- .../newview/scripts/lua/require/LLChatListener.lua | 45 +++++++++++++ indra/newview/scripts/lua/require/login.lua | 19 ++++++ indra/newview/scripts/lua/require/mapargs.lua | 73 ++++++++++++++++++++++ 6 files changed, 137 insertions(+), 137 deletions(-) delete mode 100644 indra/newview/scripts/lua/LLChatListener.lua delete mode 100644 indra/newview/scripts/lua/login.lua delete mode 100644 indra/newview/scripts/lua/mapargs.lua create mode 100644 indra/newview/scripts/lua/require/LLChatListener.lua create mode 100644 indra/newview/scripts/lua/require/login.lua create mode 100644 indra/newview/scripts/lua/require/mapargs.lua diff --git a/indra/newview/scripts/lua/LLChatListener.lua b/indra/newview/scripts/lua/LLChatListener.lua deleted file mode 100644 index b4e90d272c..0000000000 --- a/indra/newview/scripts/lua/LLChatListener.lua +++ /dev/null @@ -1,45 +0,0 @@ -local fiber = require 'fiber' -local inspect = require 'inspect' -local leap = require 'leap' - -local LLChatListener = {} -local waitfor = {} -local listener_name = {} - -function LLChatListener:new() - local obj = setmetatable({}, self) - self.__index = self - obj.name = 'Chat_listener' - - return obj -end - -function LLChatListener:handleMessages(event_data) - print(inspect(event_data)) - return true -end - -function LLChatListener:start() - waitfor = leap.WaitFor:new(-1, self.name) - function waitfor:filter(pump, data) - if pump == "LLNearbyChat" then - return data - end - end - - fiber.launch(self.name, function() - event = waitfor:wait() - while event and self:handleMessages(event) do - event = waitfor:wait() - end - end) - - listener_name = leap.request(leap.cmdpump(), {op='listen', source='LLNearbyChat', listener="ChatListener", tweak=true}).listener -end - -function LLChatListener:stop() - leap.send(leap.cmdpump(), {op='stoplistening', source='LLNearbyChat', listener=listener_name}) - waitfor:close() -end - -return LLChatListener diff --git a/indra/newview/scripts/lua/login.lua b/indra/newview/scripts/lua/login.lua deleted file mode 100644 index 0d8591cace..0000000000 --- a/indra/newview/scripts/lua/login.lua +++ /dev/null @@ -1,19 +0,0 @@ -local UI = require 'UI' -local leap = require 'leap' - -local function login(username, password) - if username and password then - local userpath = '//username_combo/Combo Text Entry' - local passpath = '//password_edit' - -- first clear anything presently in those text fields - for _, path in pairs({userpath, passpath}) do - UI.click(path) - UI.keypress{keysym='Backsp', path=path} - end - UI.type{path=userpath, text=username} - UI.type{path=passpath, text=password} - end - leap.send('LLPanelLogin', {op='onClickConnect'}) -end - -return login diff --git a/indra/newview/scripts/lua/mapargs.lua b/indra/newview/scripts/lua/mapargs.lua deleted file mode 100644 index 45f5a9c556..0000000000 --- a/indra/newview/scripts/lua/mapargs.lua +++ /dev/null @@ -1,73 +0,0 @@ --- Allow a calling function to be passed a mix of positional arguments with --- keyword arguments. Reference them as fields of a table. --- Don't use this for a function that can accept a single table argument. --- mapargs() assumes that a single table argument means its caller was called --- with f{table constructor} syntax, and maps that table to the specified names. --- Usage: --- function f(...) --- local a = mapargs({'a1', 'a2', 'a3'}, ...) --- ... a.a1 ... etc. --- end --- f(10, 20, 30) -- a.a1 == 10, a.a2 == 20, a.a3 == 30 --- f{10, 20, 30} -- a.a1 == 10, a.a2 == 20, a.a3 == 30 --- f{a3=300, a1=100} -- a.a1 == 100, a.a2 == nil, a.a3 == 300 --- f{1, a3=3} -- a.a1 == 1, a.a2 == nil, a.a3 == 3 --- f{a3=3, 1} -- a.a1 == 1, a.a2 == nil, a.a3 == 3 -local function mapargs(names, ...) - local args = table.pack(...) - local posargs = {} - local keyargs = {} - -- For a mixed table, no Lua operation will reliably tell you how many - -- array items it contains, if there are any holes. Track that by hand. - -- We must be able to handle f(1, nil, 3) calls. - local maxpos = 0 - - -- For convenience, allow passing 'names' as a string 'n0,n1,...' - if type(names) == 'string' then - names = string.split(names, ',') - end - - if not (args.n == 1 and type(args[1]) == 'table') then - -- If caller passes more than one argument, or if the first argument - -- is not a table, then it's classic positional function-call syntax: - -- f(first, second, etc.). In that case we need not bother teasing - -- apart positional from keyword arguments. - posargs = args - maxpos = args.n - else - -- Single table argument implies f{mixed} syntax. - -- Tease apart positional arguments from keyword arguments. - for k, v in pairs(args[1]) do - if type(k) == 'number' then - posargs[k] = v - maxpos = math.max(maxpos, k) - else - if table.find(names, k) == nil then - error('unknown keyword argument ' .. tostring(k)) - end - keyargs[k] = v - end - end - end - - -- keyargs already has keyword arguments in place, just fill in positionals - args = keyargs - -- Don't exceed the number of parameter names. Loop explicitly over every - -- index value instead of using ipairs() so we can support holes (nils) in - -- posargs. - for i = 1, math.min(#names, maxpos) do - if posargs[i] ~= nil then - -- As in Python, make it illegal to pass an argument both positionally - -- and by keyword. This implementation permits func(17, first=nil), a - -- corner case about which I don't particularly care. - if args[names[i]] ~= nil then - error(string.format('parameter %s passed both positionally and by keyword', - tostring(names[i]))) - end - args[names[i]] = posargs[i] - end - end - return args -end - -return mapargs diff --git a/indra/newview/scripts/lua/require/LLChatListener.lua b/indra/newview/scripts/lua/require/LLChatListener.lua new file mode 100644 index 0000000000..b4e90d272c --- /dev/null +++ b/indra/newview/scripts/lua/require/LLChatListener.lua @@ -0,0 +1,45 @@ +local fiber = require 'fiber' +local inspect = require 'inspect' +local leap = require 'leap' + +local LLChatListener = {} +local waitfor = {} +local listener_name = {} + +function LLChatListener:new() + local obj = setmetatable({}, self) + self.__index = self + obj.name = 'Chat_listener' + + return obj +end + +function LLChatListener:handleMessages(event_data) + print(inspect(event_data)) + return true +end + +function LLChatListener:start() + waitfor = leap.WaitFor:new(-1, self.name) + function waitfor:filter(pump, data) + if pump == "LLNearbyChat" then + return data + end + end + + fiber.launch(self.name, function() + event = waitfor:wait() + while event and self:handleMessages(event) do + event = waitfor:wait() + end + end) + + listener_name = leap.request(leap.cmdpump(), {op='listen', source='LLNearbyChat', listener="ChatListener", tweak=true}).listener +end + +function LLChatListener:stop() + leap.send(leap.cmdpump(), {op='stoplistening', source='LLNearbyChat', listener=listener_name}) + waitfor:close() +end + +return LLChatListener diff --git a/indra/newview/scripts/lua/require/login.lua b/indra/newview/scripts/lua/require/login.lua new file mode 100644 index 0000000000..0d8591cace --- /dev/null +++ b/indra/newview/scripts/lua/require/login.lua @@ -0,0 +1,19 @@ +local UI = require 'UI' +local leap = require 'leap' + +local function login(username, password) + if username and password then + local userpath = '//username_combo/Combo Text Entry' + local passpath = '//password_edit' + -- first clear anything presently in those text fields + for _, path in pairs({userpath, passpath}) do + UI.click(path) + UI.keypress{keysym='Backsp', path=path} + end + UI.type{path=userpath, text=username} + UI.type{path=passpath, text=password} + end + leap.send('LLPanelLogin', {op='onClickConnect'}) +end + +return login diff --git a/indra/newview/scripts/lua/require/mapargs.lua b/indra/newview/scripts/lua/require/mapargs.lua new file mode 100644 index 0000000000..45f5a9c556 --- /dev/null +++ b/indra/newview/scripts/lua/require/mapargs.lua @@ -0,0 +1,73 @@ +-- Allow a calling function to be passed a mix of positional arguments with +-- keyword arguments. Reference them as fields of a table. +-- Don't use this for a function that can accept a single table argument. +-- mapargs() assumes that a single table argument means its caller was called +-- with f{table constructor} syntax, and maps that table to the specified names. +-- Usage: +-- function f(...) +-- local a = mapargs({'a1', 'a2', 'a3'}, ...) +-- ... a.a1 ... etc. +-- end +-- f(10, 20, 30) -- a.a1 == 10, a.a2 == 20, a.a3 == 30 +-- f{10, 20, 30} -- a.a1 == 10, a.a2 == 20, a.a3 == 30 +-- f{a3=300, a1=100} -- a.a1 == 100, a.a2 == nil, a.a3 == 300 +-- f{1, a3=3} -- a.a1 == 1, a.a2 == nil, a.a3 == 3 +-- f{a3=3, 1} -- a.a1 == 1, a.a2 == nil, a.a3 == 3 +local function mapargs(names, ...) + local args = table.pack(...) + local posargs = {} + local keyargs = {} + -- For a mixed table, no Lua operation will reliably tell you how many + -- array items it contains, if there are any holes. Track that by hand. + -- We must be able to handle f(1, nil, 3) calls. + local maxpos = 0 + + -- For convenience, allow passing 'names' as a string 'n0,n1,...' + if type(names) == 'string' then + names = string.split(names, ',') + end + + if not (args.n == 1 and type(args[1]) == 'table') then + -- If caller passes more than one argument, or if the first argument + -- is not a table, then it's classic positional function-call syntax: + -- f(first, second, etc.). In that case we need not bother teasing + -- apart positional from keyword arguments. + posargs = args + maxpos = args.n + else + -- Single table argument implies f{mixed} syntax. + -- Tease apart positional arguments from keyword arguments. + for k, v in pairs(args[1]) do + if type(k) == 'number' then + posargs[k] = v + maxpos = math.max(maxpos, k) + else + if table.find(names, k) == nil then + error('unknown keyword argument ' .. tostring(k)) + end + keyargs[k] = v + end + end + end + + -- keyargs already has keyword arguments in place, just fill in positionals + args = keyargs + -- Don't exceed the number of parameter names. Loop explicitly over every + -- index value instead of using ipairs() so we can support holes (nils) in + -- posargs. + for i = 1, math.min(#names, maxpos) do + if posargs[i] ~= nil then + -- As in Python, make it illegal to pass an argument both positionally + -- and by keyword. This implementation permits func(17, first=nil), a + -- corner case about which I don't particularly care. + if args[names[i]] ~= nil then + error(string.format('parameter %s passed both positionally and by keyword', + tostring(names[i]))) + end + args[names[i]] = posargs[i] + end + end + return args +end + +return mapargs -- cgit v1.2.3 From 63e7e0b35dff15a2e06b924945dbb09389723686 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Fri, 21 Jun 2024 10:10:35 -0400 Subject: Use util.classctor(LLChatListener). --- indra/newview/scripts/lua/require/LLChatListener.lua | 3 +++ indra/newview/scripts/lua/test_LLChatListener.lua | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/indra/newview/scripts/lua/require/LLChatListener.lua b/indra/newview/scripts/lua/require/LLChatListener.lua index b4e90d272c..428dca881e 100644 --- a/indra/newview/scripts/lua/require/LLChatListener.lua +++ b/indra/newview/scripts/lua/require/LLChatListener.lua @@ -1,6 +1,7 @@ local fiber = require 'fiber' local inspect = require 'inspect' local leap = require 'leap' +local util = require 'util' local LLChatListener = {} local waitfor = {} @@ -14,6 +15,8 @@ function LLChatListener:new() return obj end +util.classctor(LLChatListener) + function LLChatListener:handleMessages(event_data) print(inspect(event_data)) return true diff --git a/indra/newview/scripts/lua/test_LLChatListener.lua b/indra/newview/scripts/lua/test_LLChatListener.lua index b9696e7cfc..18363ed43b 100644 --- a/indra/newview/scripts/lua/test_LLChatListener.lua +++ b/indra/newview/scripts/lua/test_LLChatListener.lua @@ -11,7 +11,7 @@ function openOrEcho(message) end end -local listener = LLChatListener:new() +local listener = LLChatListener() function listener:handleMessages(event_data) if string.find(event_data.message, '[LUA]') then -- cgit v1.2.3 From e05155494cb3a4b24f9e89252e34953e68eb7107 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Fri, 21 Jun 2024 14:25:05 -0400 Subject: Multiple LL.atexit(function) calls run functions in reverse order. --- indra/newview/scripts/lua/test_atexit.lua | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 indra/newview/scripts/lua/test_atexit.lua diff --git a/indra/newview/scripts/lua/test_atexit.lua b/indra/newview/scripts/lua/test_atexit.lua new file mode 100644 index 0000000000..6fbc0f3eb1 --- /dev/null +++ b/indra/newview/scripts/lua/test_atexit.lua @@ -0,0 +1,3 @@ +LL.atexit(function() print('Third') end) +LL.atexit(function() print('Second') end) +LL.atexit(function() print('First') end) -- cgit v1.2.3 From 1c19563a76dd227c80693701fdc2d52d65bbf4f4 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Fri, 21 Jun 2024 14:26:36 -0400 Subject: Introduce require/logout.lua and test_logout.lua. Add "userQuit" operation to LLAppViewerListener to engage LLAppViewer::userQuit(), which pops up "Are you sure?" prompt unless suppressed. --- indra/newview/llappviewerlistener.cpp | 9 +++++++++ indra/newview/llappviewerlistener.h | 5 +++-- indra/newview/scripts/lua/require/logout.lua | 7 +++++++ indra/newview/scripts/lua/test_logout.lua | 3 +++ 4 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 indra/newview/scripts/lua/require/logout.lua create mode 100644 indra/newview/scripts/lua/test_logout.lua diff --git a/indra/newview/llappviewerlistener.cpp b/indra/newview/llappviewerlistener.cpp index 6d519b6fef..d02b1b4a79 100644 --- a/indra/newview/llappviewerlistener.cpp +++ b/indra/newview/llappviewerlistener.cpp @@ -42,6 +42,9 @@ LLAppViewerListener::LLAppViewerListener(const LLAppViewerGetter& getter): mAppViewerGetter(getter) { // add() every method we want to be able to invoke via this event API. + add("userQuit", + "Ask to quit with user confirmation prompt", + &LLAppViewerListener::userQuit); add("requestQuit", "Ask to quit nicely", &LLAppViewerListener::requestQuit); @@ -50,6 +53,12 @@ LLAppViewerListener::LLAppViewerListener(const LLAppViewerGetter& getter): &LLAppViewerListener::forceQuit); } +void LLAppViewerListener::userQuit(const LLSD& event) +{ + LL_INFOS() << "Listener requested user quit" << LL_ENDL; + mAppViewerGetter()->userQuit(); +} + void LLAppViewerListener::requestQuit(const LLSD& event) { LL_INFOS() << "Listener requested quit" << LL_ENDL; diff --git a/indra/newview/llappviewerlistener.h b/indra/newview/llappviewerlistener.h index 5ade3d3e04..e116175eb7 100644 --- a/indra/newview/llappviewerlistener.h +++ b/indra/newview/llappviewerlistener.h @@ -30,7 +30,7 @@ #define LL_LLAPPVIEWERLISTENER_H #include "lleventapi.h" -#include +#include class LLAppViewer; class LLSD; @@ -39,11 +39,12 @@ class LLSD; class LLAppViewerListener: public LLEventAPI { public: - typedef boost::function LLAppViewerGetter; + typedef std::function LLAppViewerGetter; /// Bind the LLAppViewer instance to use (e.g. LLAppViewer::instance()). LLAppViewerListener(const LLAppViewerGetter& getter); private: + void userQuit(const LLSD& event); void requestQuit(const LLSD& event); void forceQuit(const LLSD& event); diff --git a/indra/newview/scripts/lua/require/logout.lua b/indra/newview/scripts/lua/require/logout.lua new file mode 100644 index 0000000000..63dcd7f01f --- /dev/null +++ b/indra/newview/scripts/lua/require/logout.lua @@ -0,0 +1,7 @@ +local leap = require 'leap' + +local function logout() + leap.send('LLAppViewer', {op='userQuit'}); +end + +return logout diff --git a/indra/newview/scripts/lua/test_logout.lua b/indra/newview/scripts/lua/test_logout.lua new file mode 100644 index 0000000000..b1ac59e38c --- /dev/null +++ b/indra/newview/scripts/lua/test_logout.lua @@ -0,0 +1,3 @@ +logout = require 'logout' + +logout() -- cgit v1.2.3 From a6d860f9f3bd7ce8ed5d42635a730c1ee9714aea Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Fri, 21 Jun 2024 14:47:30 -0400 Subject: login.lua works now, update test_login.lua accordingly. --- indra/newview/scripts/lua/test_login.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indra/newview/scripts/lua/test_login.lua b/indra/newview/scripts/lua/test_login.lua index 6df52b08c2..a8c31807bc 100644 --- a/indra/newview/scripts/lua/test_login.lua +++ b/indra/newview/scripts/lua/test_login.lua @@ -3,5 +3,5 @@ login = require 'login' startup.wait('STATE_LOGIN_WAIT') login() --- WIP: not working as of 2024-06-11 +-- Fill in valid credentials as they would be entered on the login screen -- login('My Username', 'password') -- cgit v1.2.3 From 8fa98f095a89778cd3153f3ee88f5626fc5a0d02 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Fri, 21 Jun 2024 14:50:35 -0400 Subject: Remove pre-Floater.lua versions of the floater test scripts. --- indra/newview/scripts/lua/test_luafloater_demo.lua | 77 ---------------------- .../scripts/lua/test_luafloater_gesture_list.lua | 75 --------------------- 2 files changed, 152 deletions(-) delete mode 100644 indra/newview/scripts/lua/test_luafloater_demo.lua delete mode 100644 indra/newview/scripts/lua/test_luafloater_gesture_list.lua diff --git a/indra/newview/scripts/lua/test_luafloater_demo.lua b/indra/newview/scripts/lua/test_luafloater_demo.lua deleted file mode 100644 index 65a31670c8..0000000000 --- a/indra/newview/scripts/lua/test_luafloater_demo.lua +++ /dev/null @@ -1,77 +0,0 @@ -XML_FILE_PATH = LL.abspath("luafloater_demo.xml") - -scriptparts = string.split(LL.source_path(), '/') -scriptname = scriptparts[#scriptparts] -print('Running ' .. scriptname) - -leap = require 'leap' -fiber = require 'fiber' -startup = require 'startup' - ---event pump for sending actions to the floater -local COMMAND_PUMP_NAME = "" -local reqid ---table of floater UI events -event_list=leap.request("LLFloaterReg", {op="getFloaterEvents"}).events - -local function _event(event_name) - if not table.find(event_list, event_name) then - LL.print_warning("Incorrect event name: " .. event_name) - end - return event_name -end - -function post(action) - leap.send(COMMAND_PUMP_NAME, action) -end - -function getCurrentTime() - local currentTime = os.date("*t") - return string.format("%02d:%02d:%02d", currentTime.hour, currentTime.min, currentTime.sec) -end - -function handleEvents(event_data) - post({action="add_text", ctrl_name="events_editor", value = event_data}) - if event_data.event == _event("commit") then - if event_data.ctrl_name == "disable_ctrl" then - post({action="set_enabled", ctrl_name="open_btn", value = (1 - event_data.value)}) - elseif event_data.ctrl_name == "title_cmb" then - post({action="set_title", value= event_data.value}) - elseif event_data.ctrl_name == "open_btn" then - floater_name = leap.request(COMMAND_PUMP_NAME, {action="get_value", ctrl_name='openfloater_cmd'})['value'] - leap.send("LLFloaterReg", {name = floater_name, op = "showInstance"}) - end - elseif event_data.event == _event("double_click") then - if event_data.ctrl_name == "show_time_lbl" then - post({action="set_value", ctrl_name="time_lbl", value= getCurrentTime()}) - end - elseif event_data.event == _event("floater_close") then - LL.print_warning("Floater was closed") - return false - end - return true -end - -startup.wait('STATE_LOGIN_WAIT') -local key = {xml_path = XML_FILE_PATH, op = "showLuaFloater"} ---sign for additional events for defined control {= {action1, action2, ...}} -key.extra_events={show_time_lbl = {_event("right_mouse_down"), _event("double_click")}} -local resp = leap.request("LLFloaterReg", key) -COMMAND_PUMP_NAME = resp.command_name -reqid = resp.reqid - -catch_events = leap.WaitFor(-1, "all_events") -function catch_events:filter(pump, data) - if data.reqid == reqid then - return data - end -end - -function process_events(waitfor) - event_data = waitfor:wait() - while event_data and handleEvents(event_data) do - event_data = waitfor:wait() - end -end - -fiber.launch("catch_events", process_events, catch_events) diff --git a/indra/newview/scripts/lua/test_luafloater_gesture_list.lua b/indra/newview/scripts/lua/test_luafloater_gesture_list.lua deleted file mode 100644 index a5fd325430..0000000000 --- a/indra/newview/scripts/lua/test_luafloater_gesture_list.lua +++ /dev/null @@ -1,75 +0,0 @@ -XML_FILE_PATH = LL.abspath("luafloater_gesture_list.xml") - -scriptparts = string.split(LL.source_path(), '/') -scriptname = scriptparts[#scriptparts] -print('Running ' .. scriptname) - -leap = require 'leap' -fiber = require 'fiber' -LLGesture = require 'LLGesture' -startup = require 'startup' - ---event pump for sending actions to the floater -local COMMAND_PUMP_NAME = "" -local reqid ---table of floater UI events -event_list=leap.request("LLFloaterReg", {op="getFloaterEvents"}).events - -local function _event(event_name) - if not table.find(event_list, event_name) then - LL.print_warning("Incorrect event name: " .. event_name) - end - return event_name -end - -function post(action) - leap.send(COMMAND_PUMP_NAME, action) -end - -function handleEvents(event_data) - if event_data.event == _event("floater_close") then - return false - end - - if event_data.event == _event("post_build") then - COMMAND_PUMP_NAME = event_data.command_name - reqid = event_data.reqid - gestures_uuid = LLGesture.getActiveGestures() - local action_data = {} - action_data.action = "add_list_element" - action_data.ctrl_name = "gesture_list" - gestures = {} - for uuid, info in pairs(gestures_uuid) do - table.insert(gestures, {value = uuid, columns ={column = "gesture_name", value = info.name}}) - end - action_data.value = gestures - post(action_data) - elseif event_data.event == _event("double_click") then - if event_data.ctrl_name == "gesture_list" then - LLGesture.startGesture(event_data.value) - end - end - return true -end - -startup.wait('STATE_STARTED') -local key = {xml_path = XML_FILE_PATH, op = "showLuaFloater"} ---receive additional events for defined control {= {action1, action2, ...}} -key.extra_events={gesture_list = {_event("double_click")}} -handleEvents(leap.request("LLFloaterReg", key)) - -catch_events = leap.WaitFor(-1, "all_events") -function catch_events:filter(pump, data) - if data.reqid == reqid then - return data - end -end - -function process_events(waitfor) - event_data = waitfor:wait() - while event_data and handleEvents(event_data) do - event_data = waitfor:wait() - end -end - -fiber.launch("catch_events", process_events, catch_events) -- cgit v1.2.3 From 56e4b8c5f637343c8a1a181fd59324e033b4782d Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Fri, 21 Jun 2024 15:11:57 -0400 Subject: Exercise the simple popup.lua APIs --- indra/newview/scripts/lua/test_popup.lua | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 indra/newview/scripts/lua/test_popup.lua diff --git a/indra/newview/scripts/lua/test_popup.lua b/indra/newview/scripts/lua/test_popup.lua new file mode 100644 index 0000000000..e48f89c3a7 --- /dev/null +++ b/indra/newview/scripts/lua/test_popup.lua @@ -0,0 +1,6 @@ +popup = require 'popup' + +response = popup:alert('This just has a Close button') +response = popup:alertOK(string.format('You said "%s", is that OK?', next(response))) +response = popup:alertYesCancel(string.format('You said "%s"', next(response))) +popup:tip(string.format('You said "%s"', next(response))) -- cgit v1.2.3