From 5b04ae7812e6cb0d0c9895aec6db6a206d4e09e8 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Mon, 25 Mar 2024 17:35:28 -0400 Subject: util.lua claims functions are in alpha order - make it so. Also streamline util.contains(), given table.find(). --- indra/newview/scripts/lua/util.lua | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) (limited to 'indra') diff --git a/indra/newview/scripts/lua/util.lua b/indra/newview/scripts/lua/util.lua index 5d6042dfe5..a2191288f6 100644 --- a/indra/newview/scripts/lua/util.lua +++ b/indra/newview/scripts/lua/util.lua @@ -2,9 +2,9 @@ local util = {} --- cheap test whether table t is empty -function util.empty(t) - return not next(t) +-- 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 @@ -17,6 +17,11 @@ function util.count(t) 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 @@ -36,14 +41,4 @@ function util.equal(t1, t2) return util.empty(temp) end --- check if array-like table contains certain value -function util.contains(t, v) - for _, value in ipairs(t) do - if value == v then - return true - end - end - return false -end - return util -- cgit v1.2.3 From c618758c7c91917905b1075e29944ef70e7e9b33 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 27 Mar 2024 15:53:48 -0400 Subject: Run loaded `require()` module on Lua's main thread. The problem with running a `require()` module on a Lua coroutine is that it prohibits calling `leap.request()` at module load time. When a coroutine calls `leap.request()`, it must yield back to Lua's main thread -- but a `require()` module is forbidden from yielding. Running on Lua's main thread means that (after potentially giving time slices to other ready coroutines) `fiber.lua` will request the response event from the viewer, and continue processing the loaded module without having to yield. --- indra/newview/llluamanager.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) (limited to 'indra') diff --git a/indra/newview/llluamanager.cpp b/indra/newview/llluamanager.cpp index c65f062fd7..aed6aee239 100644 --- a/indra/newview/llluamanager.cpp +++ b/indra/newview/llluamanager.cpp @@ -508,12 +508,14 @@ void LLRequireResolver::runModule(const std::string& desc, const std::string& co // 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); +// 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); +// lua_xmove(GL, L, 1); // new thread needs to have the globals sandboxed - luaL_sandboxthread(ML); +// luaL_sandboxthread(ML); { // If loadstring() returns (! LUA_OK) then there's an error message on @@ -523,7 +525,9 @@ void LLRequireResolver::runModule(const std::string& desc, const std::string& co { // 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); +// int status = lua_resume(ML, L, 0); + // we expect one return value + int status = lua_pcall(ML, 0, 1, 0); if (status == LUA_OK) { @@ -545,9 +549,12 @@ void LLRequireResolver::runModule(const std::string& desc, const std::string& co } // There's now a return value (string error message or module) on top of ML. // Move return value to L's stack. - lua_xmove(ML, L, 1); + if (ML != L) + { + lua_xmove(ML, L, 1); + } // remove ML from L's stack - lua_remove(L, -2); +// 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 58d5e288e0bfaa9819b68b376767a8a39a97fef8 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 27 Mar 2024 16:20:36 -0400 Subject: poetry --- indra/newview/scripts/lua/Queue.lua | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'indra') diff --git a/indra/newview/scripts/lua/Queue.lua b/indra/newview/scripts/lua/Queue.lua index b0a5a87f87..5ab2a8a72c 100644 --- a/indra/newview/scripts/lua/Queue.lua +++ b/indra/newview/scripts/lua/Queue.lua @@ -1,6 +1,12 @@ -- 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 Queue = {} function Queue:new() -- cgit v1.2.3 From af38e606865c2ed49a13bd6bd91d9604997798c8 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 27 Mar 2024 16:38:45 -0400 Subject: Enhance Lua debugging output. Don't use "debug" as the name of a function to conditionally write debug messages: "debug" is a Luau built-in library, and assigning that name locally would shadow the builtin. Use "dbg" instead. Recast fiber.print_all() as fiber.format_all() that returns a string; then print_all() is simply print(format_all()). This refactoring allows us to use dbg(format_all()) as well. Add a couple new dbg() messages at fiber state changes. --- indra/newview/scripts/lua/ErrorQueue.lua | 8 +++--- indra/newview/scripts/lua/WaitQueue.lua | 11 ++++---- indra/newview/scripts/lua/fiber.lua | 45 +++++++++++++++++++------------- indra/newview/scripts/lua/leap.lua | 26 +++++++++--------- 4 files changed, 50 insertions(+), 40 deletions(-) (limited to 'indra') diff --git a/indra/newview/scripts/lua/ErrorQueue.lua b/indra/newview/scripts/lua/ErrorQueue.lua index 076742815a..6ed1c10d5c 100644 --- a/indra/newview/scripts/lua/ErrorQueue.lua +++ b/indra/newview/scripts/lua/ErrorQueue.lua @@ -3,22 +3,22 @@ -- raise that error. local WaitQueue = require('WaitQueue') --- local debug = require('printf') -local function debug(...) end +-- local dbg = require('printf') +local function dbg(...) end local ErrorQueue = WaitQueue:new() function ErrorQueue:Error(message) -- Setting Error() is a marker, like closing the queue. Once we reach the -- error, every subsequent Dequeue() call will raise the same error. - debug('Setting self._closed to %q', message) + dbg('Setting self._closed to %q', message) self._closed = message self:_wake_waiters() end function ErrorQueue:Dequeue() local value = WaitQueue.Dequeue(self) - debug('ErrorQueue:Dequeue: base Dequeue() got %s', value) + dbg('ErrorQueue:Dequeue: base Dequeue() got %s', value) if value ~= nil then -- queue not yet closed, show caller return value diff --git a/indra/newview/scripts/lua/WaitQueue.lua b/indra/newview/scripts/lua/WaitQueue.lua index 1fbcc50847..ad4fdecf43 100644 --- a/indra/newview/scripts/lua/WaitQueue.lua +++ b/indra/newview/scripts/lua/WaitQueue.lua @@ -5,8 +5,8 @@ local fiber = require('fiber') local Queue = require('Queue') --- local debug = LL.print_debug -local function debug(...) end +-- local dbg = require('printf') +local function dbg(...) end local WaitQueue = Queue:new() @@ -60,17 +60,18 @@ function WaitQueue:Dequeue() -- the queue while there are still items left, and we want the -- consumer(s) to retrieve those last few items. if self._closed then - debug('WaitQueue:Dequeue(): closed') + dbg('WaitQueue:Dequeue(): closed') return nil end - debug('WaitQueue:Dequeue(): waiting') + 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 - debug('WaitQueue:Dequeue() calling Queue.Dequeue()') + dbg('WaitQueue:Dequeue() calling Queue.Dequeue()') return Queue.Dequeue(self) end diff --git a/indra/newview/scripts/lua/fiber.lua b/indra/newview/scripts/lua/fiber.lua index aebf27357f..9057e6c890 100644 --- a/indra/newview/scripts/lua/fiber.lua +++ b/indra/newview/scripts/lua/fiber.lua @@ -17,8 +17,8 @@ -- or with an error). local printf = require 'printf' --- local debug = printf -local function debug(...) end +-- local dbg = printf +local function dbg(...) end local coro = require 'coro' local fiber = {} @@ -78,22 +78,28 @@ function fiber.launch(name, func, ...) byname[namekey] = co -- and remember it as this fiber's name names[co] = namekey --- debug('launch(%s)', namekey) --- debug('byname[%s] = %s', namekey, tostring(byname[namekey])) --- debug('names[%s] = %s', tostring(co), names[co]) --- debug('ready[-1] = %s', tostring(ready[#ready])) +-- 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 fiber.print_all() - print('Ready fibers:' .. if next(ready) then '' else ' none') +function format_all() + output = {} + table.insert(output, 'Ready fibers:' .. if next(ready) then '' else ' none') for _, co in pairs(ready) do - printf(' %s: %s', fiber.get_name(co), fiber.status(co)) + table.insert(output, string.format(' %s: %s', fiber.get_name(co), fiber.status(co))) end - print('Waiting fibers:' .. if next(waiting) then '' else ' none') + table.insert(output, 'Waiting fibers:' .. if next(waiting) then '' else ' none') for co in pairs(waiting) do - printf(' %s: %s', fiber.get_name(co), fiber.status(co)) + 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, @@ -160,6 +166,7 @@ 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() @@ -175,26 +182,27 @@ function fiber.wake(co) 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 + -- 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 - debug('%s live_ready_iter() sees %s, status %s', + 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 - debug('%s live_ready_iter() returning %s', + dbg('%s live_ready_iter() returning %s', fiber.get_name(), fiber.get_name(co)) return co end end - debug('%s live_ready_iter() returning nil', fiber.get_name()) + dbg('%s live_ready_iter() returning nil', fiber.get_name()) return nil end @@ -214,6 +222,7 @@ end -- * 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 @@ -311,12 +320,12 @@ function fiber.run() end local others, idle_done repeat - debug('%s calling fiber.run() calling scheduler()', fiber.get_name()) + dbg('%s calling fiber.run() calling scheduler()', fiber.get_name()) others, idle_done = scheduler() - debug("%s fiber.run()'s scheduler() returned %s, %s", fiber.get_name(), + dbg("%s fiber.run()'s scheduler() returned %s, %s", fiber.get_name(), tostring(others), tostring(idle_done)) until (not others) - debug('%s fiber.run() done', fiber.get_name()) + 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 diff --git a/indra/newview/scripts/lua/leap.lua b/indra/newview/scripts/lua/leap.lua index a60819d493..9da1839c68 100644 --- a/indra/newview/scripts/lua/leap.lua +++ b/indra/newview/scripts/lua/leap.lua @@ -40,8 +40,8 @@ local fiber = require('fiber') local ErrorQueue = require('ErrorQueue') --- local debug = require('printf') -local function debug(...) end +-- local dbg = require('printf') +local function dbg(...) end local leap = {} @@ -102,7 +102,7 @@ end -- -- See also request(), generate(). function leap.send(pump, data, reqid) - debug('leap.send(%s, %s, %s) entry', pump, data, reqid) + dbg('leap.send(%s, %s, %s) entry', pump, data, reqid) local data = data if type(data) == 'table' then data = table.clone(data) @@ -111,7 +111,7 @@ function leap.send(pump, data, reqid) data['reqid'] = reqid end end - debug('leap.send(%s, %s) calling post_on()', pump, data) + dbg('leap.send(%s, %s) calling post_on()', pump, data) LL.post_on(pump, data) end @@ -126,7 +126,7 @@ local function requestSetup(pump, data) -- WaitForReqid object in leap._pending so dispatch() can find it. leap._pending[reqid] = leap.WaitForReqid:new(reqid) -- Pass reqid to send() to stamp it into (a copy of) the request data. - debug('requestSetup(%s, %s)', pump, data) + dbg('requestSetup(%s, %s)', pump, data) leap.send(pump, data, reqid) return reqid end @@ -152,9 +152,9 @@ end function leap.request(pump, data) local reqid = requestSetup(pump, data) local waitfor = leap._pending[reqid] - debug('leap.request(%s, %s) about to wait on %s', pump, data, tostring(waitfor)) + dbg('leap.request(%s, %s) about to wait on %s', pump, data, tostring(waitfor)) local ok, response = pcall(waitfor.wait, waitfor) - debug('leap.request(%s, %s) got %s: %s', pump, data, ok, response) + dbg('leap.request(%s, %s) got %s: %s', pump, data, ok, response) -- kill off temporary WaitForReqid object, even if error leap._pending[reqid] = nil if ok then @@ -210,7 +210,7 @@ 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(leap._waitfors) do - debug('unsolicited() checking %s', waitfor.name) + dbg('unsolicited() checking %s', waitfor.name) if waitfor:handle(pump, data) then return end @@ -243,9 +243,9 @@ fiber.set_idle(function () cleanup('done') return 'done' end - debug('leap.idle() calling get_event_next()') + dbg('leap.idle() calling get_event_next()') local ok, pump, data = pcall(LL.get_event_next) - debug('leap.idle() got %s: %s, %s', ok, pump, data) + 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) @@ -368,9 +368,9 @@ end -- Block the calling coroutine until a suitable unsolicited event (one -- for which filter() returns the event) arrives. function leap.WaitFor:wait() - debug('%s about to wait', self.name) + dbg('%s about to wait', self.name) local item = self._queue:Dequeue() - debug('%s got %s', self.name, item) + dbg('%s got %s', self.name, item) return item end @@ -392,7 +392,7 @@ end -- called by unsolicited() for each WaitFor in leap._waitfors function leap.WaitFor:handle(pump, data) local item = self:filter(pump, data) - debug('%s.filter() returned %s', self.name, item) + 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 -- cgit v1.2.3 From f45ae0def221b4e1911e83f2da528174cf1d8a0d Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 2 Apr 2024 11:09:03 -0400 Subject: Defend leap.request(), generate() from garbage collection. Earlier we had blithely designated the 'pending' list (which stores WaitForReqid objects for pending request() and generate() calls) as a weak table. But the caller of request() or generate() does not hold a reference to the WaitForReqid object. Make pending hold "strong" references. Private collections (pending, waitfors) and private scalars that are never reassigned (reply, command) need not be entries in the leap table. --- indra/newview/scripts/lua/leap.lua | 77 ++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 37 deletions(-) (limited to 'indra') diff --git a/indra/newview/scripts/lua/leap.lua b/indra/newview/scripts/lua/leap.lua index 9da1839c68..d19273e8bc 100644 --- a/indra/newview/scripts/lua/leap.lua +++ b/indra/newview/scripts/lua/leap.lua @@ -40,25 +40,25 @@ local fiber = require('fiber') local ErrorQueue = require('ErrorQueue') --- local dbg = require('printf') local function dbg(...) end +-- local dbg = require('printf') local leap = {} --- _reply: string name of reply LLEventPump. Any events the viewer posts to +-- 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, ...) +-- command: string name of command LLEventPump. post_to(command, ...) -- engages LLLeapListener operations such as listening on a specified other -- LLEventPump, etc. -leap._reply, leap._command = LL.get_event_pumps() +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? +-- command's "getAPI" operation. +-- For Lua, command's "getFeatures" operation suffices? -- leap._features = {} -- Each outstanding request() or generate() call has a corresponding @@ -67,20 +67,25 @@ leap._reply, leap._command = LL.get_event_pumps() -- we can look up the appropriate WaitForReqid object more efficiently -- in a dict than by tossing such objects into the usual waitfors list. -- Note: the ["reqid"] must be unique, otherwise we could end up --- replacing an earlier WaitForReqid object in self.pending with a +-- 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. --- these are weak values tables -local weak_values = {__mode='v'} -leap._pending = setmetatable({}, weak_values) +-- 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. +pending = {} -- Our consumer will instantiate some number of WaitFor subclass objects. -- As these are traversed in descending priority order, we must keep -- them in a list. -leap._waitfors = setmetatable({}, weak_values) +-- 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'} +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 +-- ["reqid"] values is our very own reply pump, we can get away with -- an integer. leap._reqid = 0 -- break leap.process() loop @@ -88,12 +93,12 @@ leap._done = false -- get the name of the reply pump function leap.replypump() - return leap._reply + return reply end -- get the name of the command pump function leap.cmdpump() - return leap._command + return command end -- Fire and forget. Send the specified request LLSD, expecting no reply. @@ -102,11 +107,10 @@ end -- -- See also request(), generate(). function leap.send(pump, data, reqid) - dbg('leap.send(%s, %s, %s) entry', pump, data, reqid) local data = data if type(data) == 'table' then data = table.clone(data) - data['reply'] = leap._reply + data['reply'] = reply if reqid ~= nil then data['reqid'] = reqid end @@ -122,13 +126,14 @@ local function requestSetup(pump, data) 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 leap._waitfors list. Instead, capture the new - -- WaitForReqid object in leap._pending so dispatch() can find it. - leap._pending[reqid] = leap.WaitForReqid:new(reqid) + -- 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) + pending[reqid] = waitfor -- Pass reqid to send() to stamp it into (a copy of) the request data. dbg('requestSetup(%s, %s)', pump, data) leap.send(pump, data, reqid) - return reqid + return reqid, waitfor end -- Send the specified request LLSD, expecting exactly one reply. Block @@ -150,13 +155,12 @@ end -- -- See also send(), generate(). function leap.request(pump, data) - local reqid = requestSetup(pump, data) - local waitfor = leap._pending[reqid] + 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 - leap._pending[reqid] = nil + pending[reqid] = nil if ok then return response else @@ -178,8 +182,7 @@ 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 = requestSetup(pump, data) - local waitfor = leap._pending[reqid] + local reqid, waitfor = requestSetup(pump, data) local ok, response repeat ok, response = pcall(waitfor.wait, waitfor) @@ -189,7 +192,7 @@ function leap.generate(pump, data, checklast) coroutine.yield(response) until checklast and checklast(response) -- If we break the above loop, whether or not due to error, clean up. - leap._pending[reqid] = nil + pending[reqid] = nil if not ok then error(response) end @@ -197,10 +200,10 @@ end local function cleanup(message) -- we're done: clean up all pending coroutines - for i, waitfor in pairs(leap._pending) do + for i, waitfor in pairs(pending) do waitfor:close() end - for i, waitfor in pairs(leap._waitfors) do + for i, waitfor in pairs(waitfors) do waitfor:close() end end @@ -209,7 +212,7 @@ end 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(leap._waitfors) do + for i, waitfor in pairs(waitfors) do dbg('unsolicited() checking %s', waitfor.name) if waitfor:handle(pump, data) then return @@ -226,7 +229,7 @@ local function dispatch(pump, data) return unsolicited(pump, data) end -- have reqid; do we have a WaitForReqid? - local waitfor = leap._pending[reqid] + local waitfor = pending[reqid] if waitfor == nil then return unsolicited(pump, data) end @@ -268,17 +271,17 @@ end -- called by WaitFor.enable() local function registerWaitFor(waitfor) - table.insert(leap._waitfors, waitfor) + table.insert(waitfors, waitfor) -- keep waitfors sorted in descending order of specified priority - table.sort(leap._waitfors, + table.sort(waitfors, function (lhs, rhs) return lhs.priority > rhs.priority end) end -- called by WaitFor.disable() local function unregisterWaitFor(waitfor) - for i, w in pairs(leap._waitfors) do + for i, w in pairs(waitfors) do if w == waitfor then - leap._waitfors[i] = nil + waitfors[i] = nil break end end @@ -389,7 +392,7 @@ function leap.WaitFor:filter(pump, data) error('You must override the WaitFor.filter() method') end --- called by unsolicited() for each WaitFor in leap._waitfors +-- 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) @@ -423,8 +426,8 @@ leap.WaitForReqid = leap.WaitFor:new() function leap.WaitForReqid:new(reqid) -- priority is meaningless, since this object won't be added to the - -- priority-sorted ViewerClient.waitfors list. Use the reqid as the - -- debugging name string. + -- priority-sorted waitfors list. Use the reqid as the debugging name + -- string. local obj = leap.WaitFor:new(nil, 'WaitForReqid(' .. reqid .. ')') setmetatable(obj, self) self.__index = self -- cgit v1.2.3 From 233de4561c25df24dd787079ed7d7d98cbc40ff2 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 2 Apr 2024 11:13:37 -0400 Subject: Give LLLeapListener's "listen" operation a "tweak" argument. If specified as true, "tweak" means to tweak the specified "listener" name for uniqueness. This avoids LLEventPump::listen()'s DupListenerName exception, which causes the "listen" operation to return "status" as false. --- indra/llcommon/llleaplistener.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'indra') diff --git a/indra/llcommon/llleaplistener.cpp b/indra/llcommon/llleaplistener.cpp index 55e4752c5d..e8a4775b67 100644 --- a/indra/llcommon/llleaplistener.cpp +++ b/indra/llcommon/llleaplistener.cpp @@ -80,11 +80,13 @@ LLLeapListener::LLLeapListener(const std::string_view& caller, const Callback& c add("listen", "Listen to an existing LLEventPump named [\"source\"], with listener name\n" "[\"listener\"].\n" + "If [\"tweak\"] is specified as true, tweak listener name for uniqueness.\n" "By default, send events on [\"source\"] to the plugin, decorated\n" "with [\"pump\"]=[\"source\"].\n" "If [\"dest\"] specified, send undecorated events on [\"source\"] to the\n" "LLEventPump named [\"dest\"].\n" - "Returns [\"status\"] boolean indicating whether the connection was made.", + "Returns [\"status\"] boolean indicating whether the connection was made,\n" + "plus [\"listener\"] reporting (possibly tweaked) listener name.", &LLLeapListener::listen, need_source_listener); add("stoplistening", @@ -119,6 +121,7 @@ LLLeapListener::~LLLeapListener() // value_type, and Bad Things would happen if you copied an // LLTempBoundListener. (Destruction of the original would disconnect the // listener, invalidating every stored connection.) + LL_DEBUGS("LLLeapListener") << "~LLLeapListener(\"" << mCaller << "\")" << LL_ENDL; for (ListenersMap::value_type& pair : mListeners) { pair.second.disconnect(); @@ -155,6 +158,11 @@ void LLLeapListener::listen(const LLSD& request) std::string source_name = request["source"]; std::string dest_name = request["dest"]; std::string listener_name = request["listener"]; + if (request["tweak"].asBoolean()) + { + listener_name = LLEventPump::inventName(listener_name); + } + reply["listener"] = listener_name; LLEventPump & source = LLEventPumps::instance().obtain(source_name); -- cgit v1.2.3 From 1d1a278712298b91342b687d1b15b107ad51b4ba Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 2 Apr 2024 11:16:29 -0400 Subject: Add LL.source_path(), source_dir() Lua entry points. This helps a Lua script log its own identity, or find associated files relative to its location in the filesystem. Add more comprehensive logging around the start and end of a given Lua script, or its "p.s." fiber.run() call. --- indra/llcommon/lua_function.cpp | 45 +++++++++++++++++++++++++++++++++++++++-- indra/llcommon/lua_function.h | 3 +++ 2 files changed, 46 insertions(+), 2 deletions(-) (limited to 'indra') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index e731408c7d..96282df554 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -68,6 +68,15 @@ int lluau::loadstring(lua_State *L, const std::string &desc, const std::string & return luau_load(L, desc.data(), bytecode.get(), bytecodeSize, 0); } +std::filesystem::path 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 + lua_Debug ar; + lua_getinfo(L, 1, "s", &ar); + return ar.source; +} + /***************************************************************************** * Lua <=> C++ conversions *****************************************************************************/ @@ -484,11 +493,15 @@ bool LuaState::checkLua(const std::string& desc, int r) std::pair LuaState::expr(const std::string& desc, const std::string& text) { if (! checkLua(desc, lluau::dostring(mState, desc, text))) + { + LL_WARNS("Lua") << desc << " error: " << mError << LL_ENDL; return { -1, mError }; + } // here we believe there was no error -- did the Lua fragment leave // anything on the stack? std::pair result{ lua_gettop(mState), {} }; + LL_INFOS("Lua") << desc << " done, " << result.first << " results." << LL_ENDL; if (result.first) { // aha, at least one entry on the stack! @@ -501,6 +514,7 @@ std::pair LuaState::expr(const std::string& desc, const std::string& } catch (const std::exception& error) { + LL_WARNS("Lua") << desc << " error converting result: " << error.what() << LL_ENDL; // lua_tollsd() is designed to be called from a lua_function(), // that is, from a C++ function called by Lua. In case of error, // it throws a Lua error to be caught by the Lua runtime. expr() @@ -519,15 +533,18 @@ std::pair LuaState::expr(const std::string& desc, const std::string& else { // multiple entries on the stack + int index; try { - for (int index = 1; index <= result.first; ++index) + for (index = 1; index <= result.first; ++index) { result.second.append(lua_tollsd(mState, index)); } } catch (const std::exception& error) { + LL_WARNS("Lua") << desc << " error converting result " << index << ": " + << error.what() << LL_ENDL; // see above comments regarding lua_State's error status initLuaState(); return { -1, stringize(LLError::Log::classname(error), ": ", error.what()) }; @@ -577,9 +594,13 @@ std::pair LuaState::expr(const std::string& desc, const std::string& { // there's a fiber.run() function sitting on the top of the stack // -- call it with no arguments, discarding anything it returns - LL_DEBUGS("Lua") << "Calling fiber.run()" << LL_ENDL; + LL_INFOS("Lua") << desc << " p.s. fiber.run()" << LL_ENDL; if (! checkLua(desc, lua_pcall(mState, 0, 0, 0))) + { + LL_WARNS("Lua") << desc << " p.s. fiber.run() error: " << mError << LL_ENDL; return { -1, mError }; + } + LL_INFOS("Lua") << desc << " p.s. done." << LL_ENDL; } } // pop everything again @@ -684,6 +705,26 @@ std::pair LuaFunction::getState() return { registry, lookup }; } +/***************************************************************************** +* source_path() +*****************************************************************************/ +lua_function(source_path, "return the source path of the running Lua script") +{ + luaL_checkstack(L, 1, nullptr); + lua_pushstdstring(L, lluau::source_path(L)); + return 1; +} + +/***************************************************************************** +* source_dir() +*****************************************************************************/ +lua_function(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()); + return 1; +} + /***************************************************************************** * check_stop() *****************************************************************************/ diff --git a/indra/llcommon/lua_function.h b/indra/llcommon/lua_function.h index 868c13c3f1..785aadeb0c 100644 --- a/indra/llcommon/lua_function.h +++ b/indra/llcommon/lua_function.h @@ -18,6 +18,7 @@ #include "luau/lualib.h" #include "stringize.h" #include // std::uncaught_exceptions() +#include #include // std::shared_ptr #include // std::pair @@ -49,6 +50,8 @@ namespace lluau // terminated char arrays. int dostring(lua_State* L, const std::string& desc, const std::string& text); int loadstring(lua_State* L, const std::string& desc, const std::string& text); + + std::filesystem::path source_path(lua_State* L); } // namespace lluau std::string lua_tostdstring(lua_State* L, int index); -- cgit v1.2.3 From b610b378ee3249b572d98875a0e557cbf80c2ded Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 2 Apr 2024 11:21:13 -0400 Subject: Use LLCoros::TempStatus when Lua is waiting on get_event_next(). When enumerating C++ coroutines, it can be useful to know that a particular Lua coroutine is simply waiting for further events. --- indra/llcommon/lualistener.cpp | 1 + 1 file changed, 1 insertion(+) (limited to 'indra') diff --git a/indra/llcommon/lualistener.cpp b/indra/llcommon/lualistener.cpp index 018a31d5a8..82e32860db 100644 --- a/indra/llcommon/lualistener.cpp +++ b/indra/llcommon/lualistener.cpp @@ -104,6 +104,7 @@ LuaListener::PumpData LuaListener::getNext() { try { + LLCoros::TempStatus status("get_event_next()"); return mQueue.pop(); } catch (const LLThreadSafeQueueInterrupt&) -- cgit v1.2.3 From cc8299d153b9aa81558951cb6b1a19693fb52982 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 2 Apr 2024 11:22:45 -0400 Subject: Streamline std::filesystem::path conversions in LLRequireResolver. Make LLRequireResolver capture std::filesystem::path instances, instead of std::strings, for the path to resolve and the source directory. Store the running script's containing directory instead of calling parent_path() over and over. Demote Lua LL.post_on() logging to DEBUG level instead of INFO. --- indra/newview/llluamanager.cpp | 29 +++++++++++------------------ indra/newview/llluamanager.h | 5 +++-- 2 files changed, 14 insertions(+), 20 deletions(-) (limited to 'indra') diff --git a/indra/newview/llluamanager.cpp b/indra/newview/llluamanager.cpp index f3a8d2c51c..9059db9967 100644 --- a/indra/newview/llluamanager.cpp +++ b/indra/newview/llluamanager.cpp @@ -122,7 +122,7 @@ lua_function(post_on, "post_on(pumpname, data): post specified data to specified std::string pumpname{ lua_tostdstring(L, 1) }; LLSD data{ lua_tollsd(L, 2) }; lua_pop(L, 2); - LL_INFOS("Lua") << "post_on('" << pumpname << "', " << data << ")" << LL_ENDL; + LL_DEBUGS("Lua") << "post_on('" << pumpname << "', " << data << ")" << LL_ENDL; LLEventPumps::instance().obtain(pumpname).post(data); return 0; } @@ -314,19 +314,12 @@ void LLRequireResolver::resolveRequire(lua_State *L, std::string path) } LLRequireResolver::LLRequireResolver(lua_State *L, const std::string& path) : - mPathToResolve(path), + mPathToResolve(std::filesystem::path(path).lexically_normal()), L(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 - lua_Debug ar; - lua_getinfo(L, 1, "s", &ar); - mSourceChunkname = ar.source; + mSourceDir = lluau::source_path(L).parent_path(); - std::filesystem::path fs_path(mPathToResolve); - mPathToResolve = fs_path.lexically_normal().string(); - - if (fs_path.is_absolute()) + if (mPathToResolve.is_absolute()) luaL_argerrorL(L, 1, "cannot require a full path"); } @@ -358,8 +351,8 @@ private: // push the loaded module or throw a Lua error void LLRequireResolver::findModule() { - // If mPathToResolve is absolute, this replaces mSourceChunkname.parent_path. - auto absolutePath = (std::filesystem::path((mSourceChunkname)).parent_path() / mPathToResolve).u8string(); + // If mPathToResolve is absolute, this replaces mSourceDir. + auto absolutePath = (mSourceDir / mPathToResolve).u8string(); // Push _MODULES table on stack for checking and saving to the cache luaL_findtable(L, LUA_REGISTRYINDEX, "_MODULES", 1); @@ -375,16 +368,16 @@ void LLRequireResolver::findModule() // not already cached - prep error message just in case auto fail{ - [L=L, path=mPathToResolve]() + [L=L, path=mPathToResolve.u8string()]() { luaL_error(L, "could not find require('%s')", path.data()); }}; - if (std::filesystem::path(mPathToResolve).is_absolute()) + if (mPathToResolve.is_absolute()) { // no point searching known directories for an absolute path fail(); } - std::vector lib_paths + std::vector lib_paths { gDirUtilp->getExpandedFilename(LL_PATH_SCRIPTS, "lua"), #ifdef LL_TEST @@ -395,10 +388,10 @@ void LLRequireResolver::findModule() for (const auto& path : lib_paths) { - std::string absolutePathOpt = (std::filesystem::path(path) / mPathToResolve).u8string(); + std::string absolutePathOpt = (path / mPathToResolve).u8string(); if (absolutePathOpt.empty()) - luaL_error(L, "error requiring module '%s'", mPathToResolve.data()); + luaL_error(L, "error requiring module '%s'", mPathToResolve.u8string().data()); if (findModuleImpl(absolutePathOpt)) return; diff --git a/indra/newview/llluamanager.h b/indra/newview/llluamanager.h index a297d14502..bd581376b4 100644 --- a/indra/newview/llluamanager.h +++ b/indra/newview/llluamanager.h @@ -29,6 +29,7 @@ #include "llcoros.h" #include "llsd.h" +#include #include #include #include // std::pair @@ -88,8 +89,8 @@ class LLRequireResolver static void resolveRequire(lua_State *L, std::string path); private: - std::string mPathToResolve; - std::string mSourceChunkname; + std::filesystem::path mPathToResolve; + std::filesystem::path mSourceDir; LLRequireResolver(lua_State *L, const std::string& path); -- cgit v1.2.3 From 1866d1a450b7857da82ceafbd2c6581d87b7f334 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 2 Apr 2024 11:29:29 -0400 Subject: Add startup.lua module with startup.ensure(), wait() functions. This lets a calling script verify that it's running at the right point in the viewer's life cycle. A script that wants to interact with the SL agent wouldn't work if run from the viewer's command line -- unless it calls startup.wait("STATE_STARTED"), which pauses until login is complete. Modify test_luafloater_demo.lua and test_luafloater_gesture_list.lua to find their respective floater XUI files in the same directory as themselves. Make them both capture the reqid returned by the "showLuaFloater" operation, and filter for events bearing the same reqid. This paves the way for a given script to display more than one floater concurrently. Make test_luafloater_demo.lua (which does not require in-world resources) wait until 'STATE_LOGIN_WAIT', the point at which the viewer has presented the login screen. Make test_luafloater_gesture_list.lua (which interacts with the agent) wait until 'STATE_STARTED', the point at which the viewer is fully in world. Either or both can now be launched from the viewer's command line. --- indra/newview/scripts/lua/startup.lua | 101 +++++++++++++++++++++ indra/newview/scripts/lua/test_luafloater_demo.lua | 25 +++-- .../scripts/lua/test_luafloater_gesture_list.lua | 26 ++++-- 3 files changed, 138 insertions(+), 14 deletions(-) create mode 100644 indra/newview/scripts/lua/startup.lua (limited to 'indra') diff --git a/indra/newview/scripts/lua/startup.lua b/indra/newview/scripts/lua/startup.lua new file mode 100644 index 0000000000..4311bb9a60 --- /dev/null +++ b/indra/newview/scripts/lua/startup.lua @@ -0,0 +1,101 @@ +-- 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' + +-- --------------------------------------------------------------------------- +-- 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 = 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:new(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'}) + +-- --------------------------------------------------------------------------- +startup = {} + +-- 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/test_luafloater_demo.lua b/indra/newview/scripts/lua/test_luafloater_demo.lua index c375a2abc7..c2d16d4f88 100644 --- a/indra/newview/scripts/lua/test_luafloater_demo.lua +++ b/indra/newview/scripts/lua/test_luafloater_demo.lua @@ -1,10 +1,16 @@ -XML_FILE_PATH = "luafloater_demo.xml" +XML_FILE_PATH = LL.source_dir() .. "/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 -COMMAND_PUMP_NAME = "" +local COMMAND_PUMP_NAME = "" +local reqid --table of floater UI events event_list=leap.request("LLFloaterReg", {op="getFloaterEvents"}).events @@ -41,24 +47,29 @@ function handleEvents(event_data) end elseif event_data.event == _event("floater_close") then LL.print_warning("Floater was closed") - leap.done() + 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")}} -COMMAND_PUMP_NAME = leap.request("LLFloaterReg", key).command_name +local resp = leap.request("LLFloaterReg", key) +COMMAND_PUMP_NAME = resp.command_name +reqid = resp.reqid catch_events = leap.WaitFor:new(-1, "all_events") function catch_events:filter(pump, data) - return data + if data.reqid == reqid then + return data + end end function process_events(waitfor) event_data = waitfor:wait() - while event_data do - handleEvents(event_data) + while event_data and handleEvents(event_data) do event_data = waitfor:wait() end end diff --git a/indra/newview/scripts/lua/test_luafloater_gesture_list.lua b/indra/newview/scripts/lua/test_luafloater_gesture_list.lua index 6d4a8e0cad..a907eb2e90 100644 --- a/indra/newview/scripts/lua/test_luafloater_gesture_list.lua +++ b/indra/newview/scripts/lua/test_luafloater_gesture_list.lua @@ -1,11 +1,17 @@ -XML_FILE_PATH = "luafloater_gesture_list.xml" +XML_FILE_PATH = LL.source_dir() .. "/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 -COMMAND_PUMP_NAME = "" +local COMMAND_PUMP_NAME = "" +local reqid --table of floater UI events event_list=leap.request("LLFloaterReg", {op="getFloaterEvents"}).events @@ -22,9 +28,12 @@ end function handleEvents(event_data) if event_data.event == _event("floater_close") then - leap.done() - elseif event_data.event == _event("post_build") 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" @@ -40,8 +49,10 @@ function handleEvents(event_data) 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")}} @@ -49,13 +60,14 @@ handleEvents(leap.request("LLFloaterReg", key)) catch_events = leap.WaitFor:new(-1, "all_events") function catch_events:filter(pump, data) - return data + if data.reqid == reqid then + return data + end end function process_events(waitfor) event_data = waitfor:wait() - while event_data do - handleEvents(event_data) + while event_data and handleEvents(event_data) do event_data = waitfor:wait() end end -- cgit v1.2.3 From 7049485ebd0b997a097c12e094425d58db56e043 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 2 Apr 2024 13:25:08 -0400 Subject: Fix std::filesystem::path - to - std::string conversions on Windows. On Windows, std::filesystem::path::value_type is wchar_t, not char -- so path::string_type is std::wstring, not std::string. So while Posix path instances implicitly convert to string, Windows path instances do not. Add explicit u8string() calls. Also add LL.abspath() Lua entry point to further facilitate finding a resource file relative to the calling Lua script. Use abspath() for both test_luafloater_demo.lua and test_luafloater_gesture_list.lua. --- indra/llcommon/lua_function.cpp | 16 ++++++++++++++-- indra/newview/scripts/lua/test_luafloater_demo.lua | 2 +- .../newview/scripts/lua/test_luafloater_gesture_list.lua | 2 +- 3 files changed, 16 insertions(+), 4 deletions(-) (limited to 'indra') diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 96282df554..cd83f40e85 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -711,7 +711,7 @@ std::pair LuaFunction::getState() lua_function(source_path, "return the source path of the running Lua script") { luaL_checkstack(L, 1, nullptr); - lua_pushstdstring(L, lluau::source_path(L)); + lua_pushstdstring(L, lluau::source_path(L).u8string()); return 1; } @@ -721,7 +721,19 @@ lua_function(source_path, "return the source path of the running Lua script") lua_function(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()); + lua_pushstdstring(L, lluau::source_path(L).parent_path().u8string()); + return 1; +} + +/***************************************************************************** +* abspath() +*****************************************************************************/ +lua_function(abspath, + "for given filesystem path relative to running script, return absolute path") +{ + auto path{ lua_tostdstring(L, 1) }; + lua_pop(L, 1); + lua_pushstdstring(L, (lluau::source_path(L).parent_path() / path).u8string()); return 1; } diff --git a/indra/newview/scripts/lua/test_luafloater_demo.lua b/indra/newview/scripts/lua/test_luafloater_demo.lua index c2d16d4f88..ab638dcdd1 100644 --- a/indra/newview/scripts/lua/test_luafloater_demo.lua +++ b/indra/newview/scripts/lua/test_luafloater_demo.lua @@ -1,4 +1,4 @@ -XML_FILE_PATH = LL.source_dir() .. "/luafloater_demo.xml" +XML_FILE_PATH = LL.abspath("luafloater_demo.xml") scriptparts = string.split(LL.source_path(), '/') scriptname = scriptparts[#scriptparts] diff --git a/indra/newview/scripts/lua/test_luafloater_gesture_list.lua b/indra/newview/scripts/lua/test_luafloater_gesture_list.lua index a907eb2e90..3d9a9b0ad4 100644 --- a/indra/newview/scripts/lua/test_luafloater_gesture_list.lua +++ b/indra/newview/scripts/lua/test_luafloater_gesture_list.lua @@ -1,4 +1,4 @@ -XML_FILE_PATH = LL.source_dir() .. "/luafloater_gesture_list.xml" +XML_FILE_PATH = LL.abspath("luafloater_gesture_list.xml") scriptparts = string.split(LL.source_path(), '/') scriptname = scriptparts[#scriptparts] -- cgit v1.2.3 From 3b25bc10febc84f10348715dabc9590458923c0b Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 3 Apr 2024 11:07:36 -0400 Subject: Make ll_convert() and ll_convert_to() use std::decay_t on arg type. Among other things, this empowers ll_convert() and ll_convert_to() to accept a string literal (which might contain non-ASCII characters, e.g. __FILE__). Without this, even though we have ll_convert_impl specializations accepting const char*, passing a string literal fails because the compiler can't find a specialization specifically accepting const char[length]. --- indra/llcommon/llstring.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'indra') diff --git a/indra/llcommon/llstring.h b/indra/llcommon/llstring.h index 0eb2770004..14aa51cb4a 100644 --- a/indra/llcommon/llstring.h +++ b/indra/llcommon/llstring.h @@ -37,6 +37,7 @@ #include #include #include +#include #include "llformat.h" #include "stdtypes.h" @@ -542,7 +543,7 @@ public: template inline operator TO() const { - return ll_convert_impl()(mRef); + return ll_convert_impl>()(mRef); } }; @@ -551,7 +552,7 @@ public: template TO ll_convert_to(const FROM& in) { - return ll_convert_impl()(in); + return ll_convert_impl>()(in); } // degenerate case -- cgit v1.2.3 From e399b02e3306a249cb161f07cac578d3f2617bab Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 3 Apr 2024 12:31:43 -0400 Subject: Introduce fsyspath subclass of std::filesystem::path. Our std::strings are UTF-8 encoded, so conversion from std::string to std::filesystem::path must use UTF-8 decoding. The native Windows std::filesystem::path constructor and assignment operator accepting std::string use "native narrow encoding," which mangles path strings containing UTF-8 encoded non-ASCII characters. fsyspath's std::string constructor and assignment operator explicitly engage std::filesystem::u8path() to handle encoding. u8path() is deprecated in C++20, but once we adapt fsyspath's conversion to C++20 conventions, consuming code need not be modified. --- indra/llcommon/CMakeLists.txt | 1 + indra/llcommon/fsyspath.h | 74 +++++++++++++++++++++++++++++++++++++++++ indra/llcommon/lua_function.cpp | 6 ++-- indra/llcommon/lua_function.h | 4 +-- indra/llui/llluafloater.cpp | 8 ++--- indra/newview/llluamanager.cpp | 8 ++--- indra/newview/llluamanager.h | 6 ++-- 7 files changed, 91 insertions(+), 16 deletions(-) create mode 100644 indra/llcommon/fsyspath.h (limited to 'indra') diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index aed9ee080b..aa0b66f2f4 100644 --- a/indra/llcommon/CMakeLists.txt +++ b/indra/llcommon/CMakeLists.txt @@ -127,6 +127,7 @@ set(llcommon_HEADER_FILES commoncontrol.h ctype_workaround.h fix_macros.h + fsyspath.h function_types.h indra_constants.h lazyeventapi.h diff --git a/indra/llcommon/fsyspath.h b/indra/llcommon/fsyspath.h new file mode 100644 index 0000000000..aa4e0132bc --- /dev/null +++ b/indra/llcommon/fsyspath.h @@ -0,0 +1,74 @@ +/** + * @file fsyspath.h + * @author Nat Goodspeed + * @date 2024-04-03 + * @brief Adapt our UTF-8 std::strings for std::filesystem::path + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Copyright (c) 2024, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_FSYSPATH_H) +#define LL_FSYSPATH_H + +#include + +// While std::filesystem::path can be directly constructed from std::string on +// both Posix and Windows, that's not what we want on Windows. Per +// https://en.cppreference.com/w/cpp/filesystem/path/path: + +// ... the method of conversion to the native character set depends on the +// character type used by source. +// +// * If the source character type is char, the encoding of the source is +// assumed to be the native narrow encoding (so no conversion takes place on +// POSIX systems). +// * If the source character type is char8_t, conversion from UTF-8 to native +// filesystem encoding is used. (since C++20) +// * If the source character type is wchar_t, the input is assumed to be the +// native wide encoding (so no conversion takes places on Windows). + +// The trouble is that on Windows, from std::string ("source character type is +// char"), the "native narrow encoding" isn't UTF-8, so file paths containing +// non-ASCII characters get mangled. +// +// Once we're building with C++20, we could pass a UTF-8 std::string through a +// vector to engage std::filesystem::path's own UTF-8 conversion. But +// sigh, as of 2024-04-03 we're not yet there. +// +// Anyway, encapsulating the important UTF-8 conversions in our own subclass +// allows us to migrate forward to C++20 conventions without changing +// referencing code. + +class fsyspath: public std::filesystem::path +{ + using super = std::filesystem::path; + +public: + // default + fsyspath() {} + // construct from UTF-8 encoded std::string + fsyspath(const std::string& path): super(std::filesystem::u8path(path)) {} + // construct from UTF-8 encoded const char* + fsyspath(const char* path): super(std::filesystem::u8path(path)) {} + // construct from existing path + fsyspath(const super& path): super(path) {} + + fsyspath& operator=(const super& p) { super::operator=(p); return *this; } + fsyspath& operator=(const std::string& p) + { + super::operator=(std::filesystem::u8path(p)); + return *this; + } + fsyspath& operator=(const char* p) + { + super::operator=(std::filesystem::u8path(p)); + return *this; + } + + // shadow base-class string() method with UTF-8 aware method + std::string string() const { return super::u8string(); } +}; + +#endif /* ! defined(LL_FSYSPATH_H) */ diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index cd83f40e85..7a5668f384 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -17,13 +17,13 @@ // std headers #include #include -#include #include // std::quoted #include #include // std::unique_ptr #include // external library headers // other Linden headers +#include "fsyspath.h" #include "hexdump.h" #include "lleventcoro.h" #include "llsd.h" @@ -68,7 +68,7 @@ int lluau::loadstring(lua_State *L, const std::string &desc, const std::string & return luau_load(L, desc.data(), bytecode.get(), bytecodeSize, 0); } -std::filesystem::path lluau::source_path(lua_State* L) +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 @@ -577,7 +577,7 @@ std::pair LuaState::expr(const std::string& desc, const std::string& // the next call to lua_next." // https://www.lua.org/manual/5.1/manual.html#lua_next if (lua_type(mState, -2) == LUA_TSTRING && - std::filesystem::path(lua_tostdstring(mState, -2)).stem() == "fiber") + fsyspath(lua_tostdstring(mState, -2)).stem() == "fiber") { found = true; break; diff --git a/indra/llcommon/lua_function.h b/indra/llcommon/lua_function.h index 785aadeb0c..ec1e6cdb10 100644 --- a/indra/llcommon/lua_function.h +++ b/indra/llcommon/lua_function.h @@ -16,9 +16,9 @@ #include "luau/lua.h" #include "luau/luaconf.h" #include "luau/lualib.h" +#include "fsyspath.h" #include "stringize.h" #include // std::uncaught_exceptions() -#include #include // std::shared_ptr #include // std::pair @@ -51,7 +51,7 @@ namespace lluau int dostring(lua_State* L, const std::string& desc, const std::string& text); int loadstring(lua_State* L, const std::string& desc, const std::string& text); - std::filesystem::path source_path(lua_State* L); + fsyspath source_path(lua_State* L); } // namespace lluau std::string lua_tostdstring(lua_State* L, int index); diff --git a/indra/llui/llluafloater.cpp b/indra/llui/llluafloater.cpp index 268075b05d..e584a67a00 100644 --- a/indra/llui/llluafloater.cpp +++ b/indra/llui/llluafloater.cpp @@ -26,7 +26,7 @@ #include "llluafloater.h" -#include +#include "fsyspath.h" #include "llevents.h" #include "llcheckboxctrl.h" @@ -271,12 +271,12 @@ void LLLuaFloater::postEvent(LLSD data, const std::string &event_name) void LLLuaFloater::showLuaFloater(const LLSD &data) { - std::filesystem::path fs_path(data["xml_path"].asString()); - std::string path = fs_path.lexically_normal().string(); + fsyspath fs_path(data["xml_path"].asString()); + std::string path = fs_path.lexically_normal().u8string(); if (!fs_path.is_absolute()) { std::string lib_path = gDirUtilp->getExpandedFilename(LL_PATH_SCRIPTS, "lua"); - path = (std::filesystem::path(lib_path) / path).u8string(); + path = (fsyspath(lib_path) / path).u8string(); } LLLuaFloater *floater = new LLLuaFloater(data); diff --git a/indra/newview/llluamanager.cpp b/indra/newview/llluamanager.cpp index 9059db9967..82be85a153 100644 --- a/indra/newview/llluamanager.cpp +++ b/indra/newview/llluamanager.cpp @@ -28,6 +28,7 @@ #include "llviewerprecompiledheaders.h" #include "llluamanager.h" +#include "fsyspath.h" #include "llcoros.h" #include "llerror.h" #include "lleventcoro.h" @@ -37,7 +38,6 @@ #include "stringize.h" #include -#include #include "luau/luacode.h" #include "luau/lua.h" @@ -314,7 +314,7 @@ void LLRequireResolver::resolveRequire(lua_State *L, std::string path) } LLRequireResolver::LLRequireResolver(lua_State *L, const std::string& path) : - mPathToResolve(std::filesystem::path(path).lexically_normal()), + mPathToResolve(fsyspath(path).lexically_normal()), L(L) { mSourceDir = lluau::source_path(L).parent_path(); @@ -377,12 +377,12 @@ void LLRequireResolver::findModule() fail(); } - std::vector lib_paths + std::vector lib_paths { gDirUtilp->getExpandedFilename(LL_PATH_SCRIPTS, "lua"), #ifdef LL_TEST // Build-time tests don't have the app bundle - use source tree. - std::filesystem::path(__FILE__).parent_path() / "scripts" / "lua", + fsyspath(__FILE__).parent_path() / "scripts" / "lua", #endif }; diff --git a/indra/newview/llluamanager.h b/indra/newview/llluamanager.h index bd581376b4..3c00450179 100644 --- a/indra/newview/llluamanager.h +++ b/indra/newview/llluamanager.h @@ -27,9 +27,9 @@ #ifndef LL_LLLUAMANAGER_H #define LL_LLLUAMANAGER_H +#include "fsyspath.h" #include "llcoros.h" #include "llsd.h" -#include #include #include #include // std::pair @@ -89,8 +89,8 @@ class LLRequireResolver static void resolveRequire(lua_State *L, std::string path); private: - std::filesystem::path mPathToResolve; - std::filesystem::path mSourceDir; + fsyspath mPathToResolve; + fsyspath mSourceDir; LLRequireResolver(lua_State *L, const std::string& path); -- cgit v1.2.3 From 3f876a6a1d138c31266afb6c39df7090e304efe3 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 3 Apr 2024 13:04:21 -0400 Subject: Use raw string literal syntax for LLLeapListener help strings. --- indra/llcommon/llleaplistener.cpp | 42 +++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) (limited to 'indra') diff --git a/indra/llcommon/llleaplistener.cpp b/indra/llcommon/llleaplistener.cpp index e8a4775b67..9b9b0f5121 100644 --- a/indra/llcommon/llleaplistener.cpp +++ b/indra/llcommon/llleaplistener.cpp @@ -70,46 +70,46 @@ LLLeapListener::LLLeapListener(const std::string_view& caller, const Callback& c { LLSD need_name(LLSDMap("name", LLSD())); add("newpump", - "Instantiate a new LLEventPump named like [\"name\"] and listen to it.\n" - "[\"type\"] == \"LLEventStream\", \"LLEventMailDrop\" et al.\n" - "Events sent through new LLEventPump will be decorated with [\"pump\"]=name.\n" - "Returns actual name in [\"name\"] (may be different if collision).", +R"-(Instantiate a new LLEventPump named like ["name"] and listen to it. +["type"] == "LLEventStream", "LLEventMailDrop" et al. +Events sent through new LLEventPump will be decorated with ["pump"]=name. +Returns actual name in ["name"] (may be different if collision).)-", &LLLeapListener::newpump, need_name); LLSD need_source_listener(LLSDMap("source", LLSD())("listener", LLSD())); add("listen", - "Listen to an existing LLEventPump named [\"source\"], with listener name\n" - "[\"listener\"].\n" - "If [\"tweak\"] is specified as true, tweak listener name for uniqueness.\n" - "By default, send events on [\"source\"] to the plugin, decorated\n" - "with [\"pump\"]=[\"source\"].\n" - "If [\"dest\"] specified, send undecorated events on [\"source\"] to the\n" - "LLEventPump named [\"dest\"].\n" - "Returns [\"status\"] boolean indicating whether the connection was made,\n" - "plus [\"listener\"] reporting (possibly tweaked) listener name.", +R"-(Listen to an existing LLEventPump named ["source"], with listener name +["listener"]. +If ["tweak"] is specified as true, tweak listener name for uniqueness. +By default, send events on ["source"] to the plugin, decorated +with ["pump"]=["source"]. +If ["dest"] specified, send undecorated events on ["source"] to the +LLEventPump named ["dest"]. +Returns ["status"] boolean indicating whether the connection was made, +plus ["listener"] reporting (possibly tweaked) listener name.)-", &LLLeapListener::listen, need_source_listener); add("stoplistening", - "Disconnect a connection previously established by \"listen\".\n" - "Pass same [\"source\"] and [\"listener\"] arguments.\n" - "Returns [\"status\"] boolean indicating whether such a listener existed.", +R"-(Disconnect a connection previously established by "listen". +Pass same ["source"] and ["listener"] arguments. +Returns ["status"] boolean indicating whether such a listener existed.)-", &LLLeapListener::stoplistening, need_source_listener); add("ping", - "No arguments, just a round-trip sanity check.", +"No arguments, just a round-trip sanity check.", &LLLeapListener::ping); add("getAPIs", - "Enumerate all LLEventAPI instances by name and description.", +"Enumerate all LLEventAPI instances by name and description.", &LLLeapListener::getAPIs); add("getAPI", - "Get name, description, dispatch key and operations for LLEventAPI [\"api\"].", +R"-(Get name, description, dispatch key and operations for LLEventAPI ["api"].)-", &LLLeapListener::getAPI, LLSD().with("api", LLSD())); add("getFeatures", - "Return an LLSD map of feature strings (deltas from baseline LEAP protocol)", +"Return an LLSD map of feature strings (deltas from baseline LEAP protocol)", static_cast(&LLLeapListener::getFeatures)); add("getFeature", - "Return the feature value with key [\"feature\"]", +R"-(Return the feature value with key ["feature"])-", &LLLeapListener::getFeature, LLSD().with("feature", LLSD())); } -- cgit v1.2.3