]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - lib/rbot/timer.rb
load rubygems if available
[user/henk/code/ruby/rbot.git] / lib / rbot / timer.rb
index 8939b6722a9e1b250978ee46981c7418cf5cb68c..8e2a6a4a656476c2ab01bbca12cbaa863ebf135c 100644 (file)
 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,
@@ -30,13 +54,15 @@ class Timer
       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
       @repeat = opts[:repeat] if opts.include? :repeat
 
       if block_given?
-        @block = block 
+        @block = block
       elsif opts[:code]
         @block = opts[:code]
       end
@@ -53,14 +79,17 @@ class Timer
       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
@@ -69,6 +98,8 @@ class Timer
       @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
@@ -77,6 +108,7 @@ class Timer
       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
@@ -87,6 +119,7 @@ class Timer
     end
   end
 
+  # creates a new Timer and starts it.
   def initialize
     self.extend(MonitorMixin)
     @tick = self.new_cond
@@ -96,6 +129,12 @@ class Timer
     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
@@ -105,15 +144,25 @@ class Timer
     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
@@ -122,6 +171,8 @@ class Timer
     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}"
@@ -130,6 +181,10 @@ class Timer
 
   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)
@@ -137,22 +192,39 @@ class Timer
     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
@@ -161,30 +233,23 @@ class Timer
   end
 
   def run_actions(now = Time.now)
-    nxt = nil
     @actions.keys.each do |k|
-      a = @actions[k]
-      next if (!a) or a.blocked?
-
-      if a.next <= now
-        begin
-          @current = k
-          v = a.run(now)
-        ensure
-          @current = nil
-        end
-          
-        unless v
-          @actions.delete k
-          next
-        end
-      else
-        v = a.next
+      return -1 if @stopping
+      a = @actions[k] or next
+      next if a.blocked? || a.next > now
+
+      begin
+        @current = k
+        a.run(now)
+      ensure
+        @current = nil
       end
 
-      nxt = v if v and ((!nxt) or (v < nxt))
+      @actions.delete k unless a.next
     end
 
+    nxt = @actions.values.find_all { |v| !v.blocked? }.map{ |v| v.next }.min
+
     if nxt
       delta = nxt - now
       delta = 0 if delta < 0