require 'thread'
require 'monitor'
+# Timer handler, manage multiple Action objects, calling them when required.
+# When the Timer is constructed, a new Thread is created to manage timed
+# delays and run Actions.
+#
+# XXX: there is no way to stop the timer currently. I'm keeping it this way
+# to weed out old Timer implementation legacy in rbot code. -jsn.
class Timer
+
+ # class representing individual timed action
class Action
+
+ # Time when the Action should be called next
attr_accessor :next
+ # Options are:
+ # start:: Time when the Action should be run for the first time.
+ # Repeatable Actions will be repeated after that, see
+ # :period. One-time Actions will not (obviously)
+ # Default: Time.now + :period
+ # period:: How often repeatable Action should be run, in seconds.
+ # Default: 1
+ # blocked:: if true, Action starts as blocked (i.e. will stay dormant
+ # until unblocked)
+ # args:: Arguments to pass to the Action callback. Default: []
+ # repeat:: Should the Action be called repeatedly? Default: false
+ # code:: You can specify the Action body using &block, *or* using
+ # this option.
+
def initialize(options = {}, &block)
opts = {
:period => 1,
debug("added #{self}")
end
+ # Provides for on-the-fly reconfiguration of the Actions
+ # Accept the same arguments as the constructor
def configure(opts = {}, &block)
@period = opts[:period] if opts.include? :period
@blocked = opts[:blocked] if opts.include? :blocked
end
end
+ # modify the Action period
def reschedule(period, &block)
self.configure(:period => period, &block)
end
+ # blocks an Action, so it won't be run
def block
@blocked = true
end
+ # unblocks a blocked Action
def unblock
@blocked = false
end
@blocked
end
+ # calls the Action callback, resets .next to the Time of the next call,
+ # if the Action is repeatable.
def run(now = Time.now)
raise 'inappropriate time to run()' unless self.next && self.next <= now
self.next = nil
rescue Exception => e
error "Timer action #{self.inspect}: block #{@block.inspect} failed!"
error e.pretty_inspect
+ debug e.backtrace.join("\n")
end
if @repeat && @period > 0
end
end
+ # creates a new Timer and starts it.
def initialize
self.extend(MonitorMixin)
@tick = self.new_cond
self.start
end
+ # Creates and installs a new Action, repeatable by default.
+ # _period_:: Action period
+ # _opts_:: options for Action#new, see there
+ # _block_:: Action callback code
+ #
+ # Returns the id of the created Action
def add(period, opts = {}, &block)
a = Action.new({:repeat => true, :period => period}.merge(opts), &block)
self.synchronize do
return a.object_id
end
+ # Creates and installs a new Action, one-time by default.
+ # _period_:: Action delay
+ # _opts_:: options for Action#new, see there
+ # _block_:: Action callback code
+ #
+ # Returns the id of the created Action
def add_once(period, opts = {}, &block)
self.add(period, {:repeat => false}.merge(opts), &block)
end
+ # blocks an existing Action
+ # _aid_:: Action id, obtained previously from add() or add_once()
def block(aid)
debug "blocking #{aid}"
self.synchronize { self[aid].block }
end
+ # unblocks an existing blocked Action
+ # _aid_:: Action id, obtained previously from add() or add_once()
def unblock(aid)
debug "unblocking #{aid}"
self.synchronize do
end
end
+ # removes an existing blocked Action
+ # _aid_:: Action id, obtained previously from add() or add_once()
def remove(aid)
self.synchronize do
@actions.delete(aid) # or raise "nonexistent action #{aid}"
alias :delete :remove
+ # Provides for on-the-fly reconfiguration of Actions
+ # _aid_:: Action id, obtained previously from add() or add_once()
+ # _opts_:: see Action#new
+ # _block_:: (optional) new Action callback code
def configure(aid, opts = {}, &block)
self.synchronize do
self[aid].configure(opts, &block)
end
end
+ # changes Action period
+ # _aid_:: Action id
+ # _period_:: new period
+ # _block_:: (optional) new Action callback code
def reschedule(aid, period, &block)
self.configure(aid, :period => period, &block)
end
- protected
-
def start
- raise 'double-started timer' if @thread
+ raise 'already started' if @thread
+ @stopping = false
+ debug "starting timer #{self}"
@thread = Thread.new do
loop do
tmout = self.run_actions
+ break if tmout and tmout < 0
self.synchronize { @tick.wait(tmout) }
end
end
end
+ def stop
+ raise 'already stopped' unless @thread
+ debug "stopping timer #{self}..."
+ @stopping = true
+ self.synchronize { @tick.signal }
+ @thread.join(60) or @thread.kill
+ debug "timer #{self} stopped"
+ @thread = nil
+ end
+
+ protected
+
def [](aid)
aid ||= @current
raise "no current action" unless aid
def run_actions(now = Time.now)
nxt = nil
@actions.keys.each do |k|
+ return -1 if @stopping
a = @actions[k]
next if (!a) or a.blocked?
ensure
@current = nil
end
-
+
unless v
@actions.delete k
next