summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--indra/llcommon/llcallbacklist.cpp14
-rw-r--r--indra/newview/scripts/lua/leap.lua59
-rw-r--r--indra/newview/scripts/lua/test_timers.lua39
-rw-r--r--indra/newview/scripts/lua/timers.lua101
4 files changed, 187 insertions, 26 deletions
diff --git a/indra/llcommon/llcallbacklist.cpp b/indra/llcommon/llcallbacklist.cpp
index 59ff8d3759..9f324b2fe9 100644
--- a/indra/llcommon/llcallbacklist.cpp
+++ b/indra/llcommon/llcallbacklist.cpp
@@ -410,11 +410,14 @@ private:
void TimersListener::scheduleAfter(const LLSD& params)
{
+ // Timer creation functions respond immediately with the reqid of the
+ // created timer, as well as later when the timer fires. That lets the
+ // requester invoke cancel, isRunning or timeUntilCall.
+ Response response(LLSD(), params);
LLSD::Real after{ params["after"] };
if (after < MINTIMER)
{
- sendReply(llsd::map("error", stringize("after must be at least ", MINTIMER)), params);
- return;
+ return response.error(stringize("after must be at least ", MINTIMER));
}
mHandles.emplace(
@@ -432,11 +435,14 @@ void TimersListener::scheduleAfter(const LLSD& params)
void TimersListener::scheduleEvery(const LLSD& params)
{
+ // Timer creation functions respond immediately with the reqid of the
+ // created timer, as well as later when the timer fires. That lets the
+ // requester invoke cancel, isRunning or timeUntilCall.
+ Response response(LLSD(), params);
LLSD::Real every{ params["every"] };
if (every < MINTIMER)
{
- sendReply(llsd::map("error", stringize("every must be at least ", MINTIMER)), params);
- return;
+ return response.error(stringize("every must be at least ", MINTIMER));
}
mHandles.emplace(
diff --git a/indra/newview/scripts/lua/leap.lua b/indra/newview/scripts/lua/leap.lua
index 69cedb4c9e..8caae24e94 100644
--- a/indra/newview/scripts/lua/leap.lua
+++ b/indra/newview/scripts/lua/leap.lua
@@ -172,36 +172,45 @@ function leap.request(pump, data)
end
-- Send the specified request LLSD, expecting an arbitrary number of replies.
--- Each one is yielded on receipt. If you omit checklast, this is an infinite
--- generator; it's up to the caller to recognize when the last reply has been
--- received, and stop resuming for more.
---
--- If you pass checklast=<callable accepting(event)>, each response event is
--- passed to that callable (after the yield). When the callable returns
--- true, the generator terminates in the usual way.
+-- 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)
- local ok, response, resumed_with
- repeat
- ok, response = pcall(waitfor.wait, waitfor)
- if (not ok) or response.error then
- break
+ 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
- -- can resume(false) to terminate generate() and clean up
- resumed_with = coroutine.yield(response)
- until (checklast and checklast(response)) or (resumed_with == false)
- -- If we break the above loop, whether or not due to error, clean up.
- pending[reqid] = nil
- if not ok then
- error(response)
- elseif response.error then
- error(response.error)
- end
+ }
end
-- Send the specified request LLSD, expecting an immediate reply followed by
@@ -220,7 +229,9 @@ function leap.eventstream(pump, data, callback)
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()
@@ -236,13 +247,17 @@ function leap.eventstream(pump, data, callback)
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
diff --git a/indra/newview/scripts/lua/test_timers.lua b/indra/newview/scripts/lua/test_timers.lua
new file mode 100644
index 0000000000..53a2dc83f2
--- /dev/null
+++ b/indra/newview/scripts/lua/test_timers.lua
@@ -0,0 +1,39 @@
+local timers = require 'timers'
+
+print('t0:new(10)')
+start = os.clock()
+t0 = timers.Timer:new(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())
+print('t0:isRunning(): ', t0:isRunning())
+print('t0:timeUntilCall(): ', t0:timeUntilCall())
+print('t0:cancel(): ', t0:cancel())
+
+print('t1:new(5)')
+start = os.clock()
+t1 = timers.Timer:new(5, function() print('t1 fired at', os.clock() - start) end)
+
+print('t2:new(2)')
+start = os.clock()
+t2 = timers.Timer:new(2)
+function t2:tick()
+ print('t2 fired at', os.clock() - start)
+end
+
+start = os.clock()
+timers.Timer:new(5, 'wait')
+print(string.format('Timer(5) waited %f seconds', os.clock() - start))
+
+start = os.clock()
+timers.Timer:new(
+ 2,
+ coroutine.wrap(function()
+ for i = 1,5 do
+ print('repeat(2) timer fired at ', os.clock() - start)
+ coroutine.yield(nil) -- keep running
+ end
+ print('repeat(2) timer fired last at ', os.clock() - start)
+ return true -- stop
+ end),
+ true) -- iterate
diff --git a/indra/newview/scripts/lua/timers.lua b/indra/newview/scripts/lua/timers.lua
new file mode 100644
index 0000000000..e0d27a680d
--- /dev/null
+++ b/indra/newview/scripts/lua/timers.lua
@@ -0,0 +1,101 @@
+-- Access to the viewer's time-delay facilities
+
+local leap = require 'leap'
+
+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
+
+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