]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/timer.rb
gettext support: initial works for language -> locale mappings
[user/henk/code/ruby/rbot.git] / lib / rbot / timer.rb
1 module Timer
2
3   # timer event, something to do and when/how often to do it
4   class Action
5
6     # when this action is due next (updated by tick())
7     attr_reader :in
8
9     # is this action blocked? if so it won't be run
10     attr_accessor :blocked
11
12     # period:: how often (seconds) to run the action
13     # data::   optional data to pass to the proc
14     # once::   optional, if true, this action will be run once then removed
15     # func::   associate a block to be called to perform the action
16     #
17     # create a new action
18     def initialize(period, data=nil, once=false, &func)
19       @blocked = false
20       @period = period
21       @in = period
22       @func = func
23       @data = data
24       @once = once
25       @last_tick = Time.new
26     end
27
28     def tick
29       diff = Time.new - @last_tick
30       @in -= diff
31       @last_tick = Time.new
32     end
33
34     def inspect
35       "#<#{self.class}:#{@period}s:#{@once ? 'once' : 'repeat'}>"
36     end
37
38     def due?
39       @in <= 0
40     end
41
42     # run the action by calling its proc
43     def run
44       @in += @period
45       # really short duration timers can overrun and leave @in negative,
46       # for these we set @in to @period
47       @in = @period if @in <= 0
48       begin
49         if(@data)
50           @func.call(@data)
51         else
52           @func.call
53         end
54       rescue Exception => e
55         error "Timer action #{self.inspect} with function #{@func.inspect} failed!"
56         error e.pretty_inspect
57         # TODO maybe we want to block this Action?
58       end
59       return @once
60     end
61
62     # reschedule the Action to change its period
63     def reschedule(new_period)
64       @period = new_period
65       @in = new_period
66     end
67   end
68
69   # timer handler, manage multiple Action objects, calling them when required.
70   # The timer must be ticked by whatever controls it, i.e. regular calls to
71   # tick() at whatever granularity suits your application's needs.
72   #
73   # Alternatively you can call run(), and the timer will spawn a thread and
74   # tick itself, intelligently shutting down the thread if there are no
75   # pending actions.
76   class Timer
77     def initialize(granularity = 0.1)
78       @granularity = granularity
79       @timers = Hash.new
80       @handle = 0
81       @lasttime = 0
82       @should_be_running = false
83       @thread = false
84       @next_action_time = 0
85     end
86
87     # period:: how often (seconds) to run the action
88     # data::   optional data to pass to the action's proc
89     # func::   associate a block with add() to perform the action
90     #
91     # add an action to the timer
92     def add(period, data=nil, &func)
93       debug "adding timer, period #{period}"
94       @handle += 1
95       @timers[@handle] = Action.new(period, data, &func)
96       start_on_add
97       return @handle
98     end
99
100     # period:: how long (seconds) until the action is run
101     # data::   optional data to pass to the action's proc
102     # func::   associate a block with add() to perform the action
103     #
104     # add an action to the timer which will be run just once, after +period+
105     def add_once(period, data=nil, &func)
106       debug "adding one-off timer, period #{period}"
107       @handle += 1
108       @timers[@handle] = Action.new(period, data, true, &func)
109       start_on_add
110       return @handle
111     end
112
113     # remove action with handle +handle+ from the timer
114     def remove(handle)
115       @timers.delete(handle)
116     end
117
118     # block action with handle +handle+
119     def block(handle)
120       raise "no such timer #{handle}" unless @timers[handle]
121       @timers[handle].blocked = true
122     end
123
124     # unblock action with handle +handle+
125     def unblock(handle)
126       raise "no such timer #{handle}" unless @timers[handle]
127       @timers[handle].blocked = false
128     end
129
130     # reschedule action with handle +handle+ to change its period
131     def reschedule(handle, period)
132       raise "no such timer #{handle}" unless @timers[handle]
133       @timers[handle].reschedule(period)
134     end
135
136     # you can call this when you know you're idle, or you can split off a
137     # thread and call the run() method to do it for you.
138     def tick
139       if(@lasttime == 0)
140         # don't do anything on the first tick
141         @lasttime = Time.now
142         return
143       end
144       @next_action_time = 0
145       diff = (Time.now - @lasttime).to_f
146       @lasttime = Time.now
147       @timers.each { |key,timer|
148         timer.tick
149         next if timer.blocked
150         if(timer.due?)
151           if(timer.run)
152             # run once
153             @timers.delete(key)
154           end
155         end
156         if @next_action_time == 0 || timer.in < @next_action_time
157           @next_action_time = timer.in
158         end
159       }
160       #debug "ticked. now #{@timers.length} timers remain"
161       #debug "next timer due at #{@next_action_time}"
162     end
163
164     # for backwards compat - this is a bit primitive
165     def run(granularity=0.1)
166       while(true)
167         sleep(granularity)
168         tick
169       end
170     end
171
172     def running?
173       @thread && @thread.alive?
174     end
175
176     # return the number of seconds until the next action is due, or 0 if
177     # none are outstanding - will only be accurate immediately after a
178     # tick()
179     def next_action_time
180       @next_action_time
181     end
182
183     # start the timer, it spawns a thread to tick the timer, intelligently
184     # shutting down if no events remain and starting again when needed.
185     def start
186       return if running?
187       @should_be_running = true
188       start_thread unless @timers.empty?
189     end
190
191     # stop the timer from ticking
192     def stop
193       @should_be_running = false
194       stop_thread
195     end
196
197     private
198
199     def start_on_add
200       if running?
201         stop_thread
202         start_thread
203       elsif @should_be_running
204         start_thread
205       end
206     end
207
208     def stop_thread
209       return unless running?
210       @thread.kill
211     end
212
213     def start_thread
214       return if running?
215       @thread = Thread.new do
216         while(true)
217           tick
218           exit if @timers.empty?
219           sleep(@next_action_time)
220         end
221       end
222     end
223
224   end
225 end