module Timer # timer event, something to do and when/how often to do it class Action # when this action is due next (updated by tick()) attr_reader :in # is this action blocked? if so it won't be run attr_accessor :blocked # period:: how often (seconds) to run the action # data:: optional data to pass to the proc # once:: optional, if true, this action will be run once then removed # func:: associate a block to be called to perform the action # # create a new action def initialize(period, data=nil, once=false, &func) @blocked = false @period = period @in = period @func = func @data = data @once = once @last_tick = Time.new end def tick diff = Time.new - @last_tick @in -= diff @last_tick = Time.new end def inspect "#<#{self.class}:#{@period}s:#{@once ? 'once' : 'repeat'}>" end def due? @in <= 0 end # run the action by calling its proc def run @in += @period # really short duration timers can overrun and leave @in negative, # for these we set @in to @period @in = @period if @in <= 0 begin if(@data) @func.call(@data) else @func.call end rescue Exception => e error "Timer action #{self.inspect} with function #{@func.inspect} failed!" error e.pretty_inspect # TODO maybe we want to block this Action? end return @once end # reschedule the Action to change its period def reschedule(new_period) @period = new_period @in = new_period end end # timer handler, manage multiple Action objects, calling them when required. # The timer must be ticked by whatever controls it, i.e. regular calls to # tick() at whatever granularity suits your application's needs. # # Alternatively you can call run(), and the timer will spawn a thread and # tick itself, intelligently shutting down the thread if there are no # pending actions. class Timer def initialize(granularity = 0.1) @granularity = granularity @timers = Hash.new @handle = 0 @lasttime = 0 @should_be_running = false @thread = false @next_action_time = 0 end # period:: how often (seconds) to run the action # data:: optional data to pass to the action's proc # func:: associate a block with add() to perform the action # # add an action to the timer def add(period, data=nil, &func) debug "adding timer, period #{period}" @handle += 1 @timers[@handle] = Action.new(period, data, &func) start_on_add return @handle end # period:: how long (seconds) until the action is run # data:: optional data to pass to the action's proc # func:: associate a block with add() to perform the action # # add an action to the timer which will be run just once, after +period+ def add_once(period, data=nil, &func) debug "adding one-off timer, period #{period}" @handle += 1 @timers[@handle] = Action.new(period, data, true, &func) start_on_add return @handle end # remove action with handle +handle+ from the timer def remove(handle) @timers.delete(handle) end # block action with handle +handle+ def block(handle) raise "no such timer #{handle}" unless @timers[handle] @timers[handle].blocked = true end # unblock action with handle +handle+ def unblock(handle) raise "no such timer #{handle}" unless @timers[handle] @timers[handle].blocked = false end # reschedule action with handle +handle+ to change its period def reschedule(handle, period) raise "no such timer #{handle}" unless @timers[handle] @timers[handle].reschedule(period) end # you can call this when you know you're idle, or you can split off a # thread and call the run() method to do it for you. def tick if(@lasttime == 0) # don't do anything on the first tick @lasttime = Time.now return end @next_action_time = 0 diff = (Time.now - @lasttime).to_f @lasttime = Time.now @timers.each { |key,timer| timer.tick next if timer.blocked if(timer.due?) if(timer.run) # run once @timers.delete(key) end end if @next_action_time == 0 || timer.in < @next_action_time @next_action_time = timer.in end } #debug "ticked. now #{@timers.length} timers remain" #debug "next timer due at #{@next_action_time}" end # for backwards compat - this is a bit primitive def run(granularity=0.1) while(true) sleep(granularity) tick end end def running? @thread && @thread.alive? end # return the number of seconds until the next action is due, or 0 if # none are outstanding - will only be accurate immediately after a # tick() def next_action_time @next_action_time end # start the timer, it spawns a thread to tick the timer, intelligently # shutting down if no events remain and starting again when needed. def start return if running? @should_be_running = true start_thread unless @timers.empty? end # stop the timer from ticking def stop @should_be_running = false stop_thread end private def start_on_add if running? stop_thread start_thread elsif @should_be_running start_thread end end def stop_thread return unless running? @thread.kill end def start_thread return if running? @thread = Thread.new do while(true) tick exit if @timers.empty? sleep(@next_action_time) end end end end end